Import translation improvements
- Don't block the UI with a progress meter during imports. Instead, show a popup in the bottom right when the import is done that shows how many items were saved. - Fix hang when importing some files - Fix various problems with asynchronous operations/transactions - Use the save queue for imports instead of creating concurrent transactions that can time out - Wait for the save to finish before returning from the translate() promise. All save modes now use the save queue, so code that handled the non-save-queue process can probably be removed. - Serialize child attachments instead of running them concurrently. This might make multi-attachment saves a little slower, since they can't download at the same time, but it avoids problems with concurrent transactions. We might be able to improve this to allow concurrent downloads, or allow concurrent saves for a limited number of items (e.g., from web saving) if not for larger imports. - Change collection handling during import, since UI is now active - Select the root collection at the beginning of the import - Assign items and collections to the root during the import instead of at the end - Don't select other collections - Change a few ItemSaver functions to use promises and remove unnecessary callbacks. (This includes some connector code that needs to be tested.) - Change some `parentID` variables in ItemSaver to `parentItemID` for clarity, since collections are now handled in more places To-do: - Save items in smaller batches instead of doing all in the same transaction - Show progress meter in a bottom-right popup during the import
This commit is contained in:
parent
c61a9dc5f3
commit
78b1d2ee35
7 changed files with 327 additions and 244 deletions
|
@ -320,44 +320,40 @@ var Zotero_File_Interface = new function() {
|
||||||
}
|
}
|
||||||
|
|
||||||
translation.setTranslator(translators[0]);
|
translation.setTranslator(translators[0]);
|
||||||
translation.setHandler("itemDone", function () {
|
// TODO: Restore a progress meter
|
||||||
|
/*translation.setHandler("itemDone", function () {
|
||||||
Zotero.updateZoteroPaneProgressMeter(translation.getProgress());
|
Zotero.updateZoteroPaneProgressMeter(translation.getProgress());
|
||||||
});
|
});*/
|
||||||
|
|
||||||
// show progress indicator
|
|
||||||
Zotero_File_Interface.Progress.show(
|
|
||||||
Zotero.getString("fileInterface.itemsImported")
|
|
||||||
);
|
|
||||||
|
|
||||||
yield Zotero.Promise.delay(0);
|
yield Zotero.Promise.delay(0);
|
||||||
|
|
||||||
let failed = false;
|
let failed = false;
|
||||||
try {
|
try {
|
||||||
yield translation.translate(libraryID);
|
yield translation.translate({
|
||||||
|
libraryID,
|
||||||
|
collections: importCollection ? [importCollection.id] : null
|
||||||
|
});
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
Zotero.logError(e);
|
Zotero.logError(e);
|
||||||
failed = true;
|
|
||||||
}
|
|
||||||
Zotero_File_Interface.Progress.close();
|
|
||||||
|
|
||||||
// Add items to import collection
|
|
||||||
if(importCollection) {
|
|
||||||
yield Zotero.DB.executeTransaction(function* () {
|
|
||||||
yield importCollection.addItems(translation.newItems.map(item => item.id));
|
|
||||||
for(let i=0; i<translation.newCollections.length; i++) {
|
|
||||||
let collection = translation.newCollections[i];
|
|
||||||
collection.parent = importCollection.id;
|
|
||||||
yield collection.save();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// // TODO: yield or change to .queue()
|
|
||||||
// Zotero.Notifier.trigger('refresh', 'collection', importCollection.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(failed) {
|
|
||||||
window.alert(Zotero.getString("fileInterface.importError"));
|
window.alert(Zotero.getString("fileInterface.importError"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show popup on completion
|
||||||
|
var numItems = translation.newItems.length;
|
||||||
|
translation.newItems.forEach(item => numItems += item.numChildren());
|
||||||
|
var progressWin = new Zotero.ProgressWindow();
|
||||||
|
progressWin.changeHeadline(Zotero.getString('fileInterface.importComplete'));
|
||||||
|
if (numItems == 1) {
|
||||||
|
var icon = translation.newItems[0].getImageSrc();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
var icon = 'chrome://zotero/skin/treesource-unfiled' + (Zotero.hiDPI ? "@2x" : "") + '.png';
|
||||||
|
}
|
||||||
|
var title = Zotero.getString(`fileInterface.itemsWereImported`, numItems, numItems);
|
||||||
|
progressWin.addLines(title, icon)
|
||||||
|
progressWin.show();
|
||||||
|
progressWin.startCloseTimer();
|
||||||
});
|
});
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
@ -73,16 +73,12 @@ Zotero.Translate.ItemSaver.prototype = {
|
||||||
/**
|
/**
|
||||||
* Saves items to Standalone or the server
|
* Saves items to Standalone or the server
|
||||||
* @param items Items in Zotero.Item.toArray() format
|
* @param items Items in Zotero.Item.toArray() format
|
||||||
* @param {Function} callback A callback to be executed when saving is complete. If saving
|
|
||||||
* succeeded, this callback will be passed true as the first argument and a list of items
|
|
||||||
* saved as the second. If saving failed, the callback will be passed false as the first
|
|
||||||
* argument and an error object as the second
|
|
||||||
* @param {Function} [attachmentCallback] A callback that receives information about attachment
|
* @param {Function} [attachmentCallback] A callback that receives information about attachment
|
||||||
* save progress. The callback will be called as attachmentCallback(attachment, false, error)
|
* save progress. The callback will be called as attachmentCallback(attachment, false, error)
|
||||||
* on failure or attachmentCallback(attachment, progressPercent) periodically during saving.
|
* on failure or attachmentCallback(attachment, progressPercent) periodically during saving.
|
||||||
*/
|
*/
|
||||||
"saveItems":function(items, callback, attachmentCallback) {
|
saveItems: function (items, attachmentCallback) {
|
||||||
var me = this;
|
var deferred = Zotero.Promise.defer();
|
||||||
// first try to save items via connector
|
// first try to save items via connector
|
||||||
var payload = {"items":items};
|
var payload = {"items":items};
|
||||||
Zotero.Connector.setCookiesThenSaveItems(payload, function(data, status) {
|
Zotero.Connector.setCookiesThenSaveItems(payload, function(data, status) {
|
||||||
|
@ -100,14 +96,15 @@ Zotero.Translate.ItemSaver.prototype = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
callback(true, items);
|
deferred.resolve(items);
|
||||||
if(haveAttachments) me._pollForProgress(items, attachmentCallback);
|
if (haveAttachments) this._pollForProgress(items, attachmentCallback);
|
||||||
} else if(Zotero.isFx) {
|
} else if(Zotero.isFx) {
|
||||||
callback(false, new Error("Save via Standalone failed with "+status));
|
deferred.reject(new Error("Save via Standalone failed with " + status));
|
||||||
} else {
|
} else {
|
||||||
me._saveToServer(items, callback, attachmentCallback);
|
deferred.resolve(this._saveToServer(items, attachmentCallback));
|
||||||
}
|
}
|
||||||
});
|
}.bind(this));
|
||||||
|
return deferred.promise;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -169,16 +166,12 @@ Zotero.Translate.ItemSaver.prototype = {
|
||||||
/**
|
/**
|
||||||
* Saves items to server
|
* Saves items to server
|
||||||
* @param items Items in Zotero.Item.toArray() format
|
* @param items Items in Zotero.Item.toArray() format
|
||||||
* @param {Function} callback A callback to be executed when saving is complete. If saving
|
|
||||||
* succeeded, this callback will be passed true as the first argument and a list of items
|
|
||||||
* saved as the second. If saving failed, the callback will be passed false as the first
|
|
||||||
* argument and an error object as the second
|
|
||||||
* @param {Function} attachmentCallback A callback that receives information about attachment
|
* @param {Function} attachmentCallback A callback that receives information about attachment
|
||||||
* save progress. The callback will be called as attachmentCallback(attachment, false, error)
|
* save progress. The callback will be called as attachmentCallback(attachment, false, error)
|
||||||
* on failure or attachmentCallback(attachment, progressPercent) periodically during saving.
|
* on failure or attachmentCallback(attachment, progressPercent) periodically during saving.
|
||||||
* attachmentCallback() will be called with all attachments that will be saved
|
* attachmentCallback() will be called with all attachments that will be saved
|
||||||
*/
|
*/
|
||||||
"_saveToServer":function(items, callback, attachmentCallback) {
|
_saveToServer: function (items, attachmentCallback) {
|
||||||
var newItems = [], itemIndices = [], typedArraysSupported = false;
|
var newItems = [], itemIndices = [], typedArraysSupported = false;
|
||||||
try {
|
try {
|
||||||
typedArraysSupported = !!(new Uint8Array(1) && new Blob());
|
typedArraysSupported = !!(new Uint8Array(1) && new Blob());
|
||||||
|
@ -197,41 +190,42 @@ Zotero.Translate.ItemSaver.prototype = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var me = this;
|
var deferred = Zotero.Promise.defer();
|
||||||
Zotero.API.createItem({"items":newItems}, function(statusCode, response) {
|
Zotero.API.createItem({"items":newItems}, function(statusCode, response) {
|
||||||
if(statusCode !== 200) {
|
if(statusCode !== 200) {
|
||||||
callback(false, new Error("Save to server failed with "+statusCode+" "+response));
|
deferred.reject(new Error("Save to server failed with " + statusCode + " " + response));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var resp = JSON.parse(response);
|
var resp = JSON.parse(response);
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
callback(false, new Error("Unexpected response received from server"));
|
deferred.reject(new Error("Unexpected response received from server"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for(var i in resp.failed) {
|
for(var i in resp.failed) {
|
||||||
callback(false, new Error("Save to server failed with "+statusCode+" "+response));
|
deferred.reject(new Error("Save to server failed with " + statusCode + " " + response));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Zotero.debug("Translate: Save to server complete");
|
Zotero.debug("Translate: Save to server complete");
|
||||||
Zotero.Prefs.getCallback(["downloadAssociatedFiles", "automaticSnapshots"],
|
Zotero.Prefs.getCallback(
|
||||||
function(prefs) {
|
["downloadAssociatedFiles", "automaticSnapshots"],
|
||||||
|
function (prefs) {
|
||||||
if(typedArraysSupported) {
|
if(typedArraysSupported) {
|
||||||
for(var i=0; i<items.length; i++) {
|
for(var i=0; i<items.length; i++) {
|
||||||
var item = items[i], key = resp.success[itemIndices[i]];
|
var item = items[i], key = resp.success[itemIndices[i]];
|
||||||
if(item.attachments && item.attachments.length) {
|
if(item.attachments && item.attachments.length) {
|
||||||
me._saveAttachmentsToServer(key, me._getFileBaseNameFromItem(item),
|
this._saveAttachmentsToServer(key, this._getFileBaseNameFromItem(item),
|
||||||
item.attachments, prefs, attachmentCallback);
|
item.attachments, prefs, attachmentCallback);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
deferred.resolve(items);
|
||||||
callback(true, items);
|
}.bind(this)
|
||||||
});
|
);
|
||||||
});
|
}.bind(this));
|
||||||
|
return deferred.promise;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -379,10 +379,11 @@ Zotero.Server.Connector.SaveItem.prototype = {
|
||||||
forceTagType: 1,
|
forceTagType: 1,
|
||||||
cookieSandbox
|
cookieSandbox
|
||||||
});
|
});
|
||||||
var deferred = Zotero.Promise.defer();
|
|
||||||
itemSaver.saveItems(data.items, function(returnValue, items) {
|
|
||||||
if(returnValue) {
|
|
||||||
try {
|
try {
|
||||||
|
let items = yield itemSaver.saveItems(
|
||||||
|
data.items,
|
||||||
|
Zotero.Server.Connector.AttachmentProgressManager.onProgress
|
||||||
|
);
|
||||||
// Remove attachments not being saved from item.attachments
|
// Remove attachments not being saved from item.attachments
|
||||||
for(var i=0; i<data.items.length; i++) {
|
for(var i=0; i<data.items.length; i++) {
|
||||||
var item = data.items[i];
|
var item = data.items[i];
|
||||||
|
@ -393,17 +394,12 @@ Zotero.Server.Connector.SaveItem.prototype = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
deferred.resolve([201, "application/json", JSON.stringify({items: data.items})]);
|
return [201, "application/json", JSON.stringify({items: data.items})];
|
||||||
} catch(e) {
|
}
|
||||||
|
catch (e) {
|
||||||
Zotero.logError(e);
|
Zotero.logError(e);
|
||||||
deferred.resolve(500);
|
return 500;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
Zotero.logError(items);
|
|
||||||
deferred.resolve(500);
|
|
||||||
}
|
|
||||||
}, Zotero.Server.Connector.AttachmentProgressManager.onProgress);
|
|
||||||
return deferred.promise;
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -199,13 +199,9 @@ Zotero.Translate.Sandbox = {
|
||||||
// Fire itemSaving event
|
// Fire itemSaving event
|
||||||
translate._runHandler("itemSaving", item);
|
translate._runHandler("itemSaving", item);
|
||||||
|
|
||||||
if(translate instanceof Zotero.Translate.Web) {
|
// TODO: This used to only be used for some modes. Since it's now used for everything with
|
||||||
// For web translators, we queue saves
|
// async saving, there's probably a bunch of code for the non-queued mode that can be removed.
|
||||||
translate.saveQueue.push(item);
|
translate.saveQueue.push(item);
|
||||||
} else {
|
|
||||||
// Save items
|
|
||||||
translate._saveItems([item]);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1480,8 +1476,7 @@ Zotero.Translate.Base.prototype = {
|
||||||
if(returnValue) {
|
if(returnValue) {
|
||||||
if(this.saveQueue.length) {
|
if(this.saveQueue.length) {
|
||||||
this._waitingForSave = true;
|
this._waitingForSave = true;
|
||||||
this._saveItems(this.saveQueue);
|
this._saveItems(this.saveQueue).then(() => this.saveQueue = []);
|
||||||
this.saveQueue = [];
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._debug("Translation successful");
|
this._debug("Translation successful");
|
||||||
|
@ -1540,15 +1535,37 @@ Zotero.Translate.Base.prototype = {
|
||||||
* Saves items to the database, taking care to defer attachmentProgress notifications
|
* Saves items to the database, taking care to defer attachmentProgress notifications
|
||||||
* until after save
|
* until after save
|
||||||
*/
|
*/
|
||||||
"_saveItems":function(items) {
|
_saveItems: function (items) {
|
||||||
var me = this,
|
var itemDoneEventsDispatched = false;
|
||||||
itemDoneEventsDispatched = false,
|
var deferredProgress = [];
|
||||||
deferredProgress = [],
|
var attachmentsWithProgress = [];
|
||||||
attachmentsWithProgress = [];
|
|
||||||
|
|
||||||
this._savingItems++;
|
this._savingItems++;
|
||||||
this._itemSaver.saveItems(items.slice(), function(returnValue, newItems) {
|
return this._itemSaver.saveItems(
|
||||||
if(returnValue) {
|
items.slice(),
|
||||||
|
function (attachment, progress, error) {
|
||||||
|
var attachmentIndex = this._savingAttachments.indexOf(attachment);
|
||||||
|
if(progress === false || progress === 100) {
|
||||||
|
if(attachmentIndex !== -1) {
|
||||||
|
this._savingAttachments.splice(attachmentIndex, 1);
|
||||||
|
}
|
||||||
|
} else if(attachmentIndex === -1) {
|
||||||
|
this._savingAttachments.push(attachment);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(itemDoneEventsDispatched) {
|
||||||
|
// itemDone event has already fired, so we can fire attachmentProgress
|
||||||
|
// notifications
|
||||||
|
this._runHandler("attachmentProgress", attachment, progress, error);
|
||||||
|
this._checkIfDone();
|
||||||
|
} else {
|
||||||
|
// Defer until after we fire the itemDone event
|
||||||
|
deferredProgress.push([attachment, progress, error]);
|
||||||
|
attachmentsWithProgress.push(attachment);
|
||||||
|
}
|
||||||
|
}.bind(this)
|
||||||
|
)
|
||||||
|
.then(function (newItems) {
|
||||||
// Remove attachments not being saved from item.attachments
|
// Remove attachments not being saved from item.attachments
|
||||||
for(var i=0; i<items.length; i++) {
|
for(var i=0; i<items.length; i++) {
|
||||||
var item = items[i];
|
var item = items[i];
|
||||||
|
@ -1561,7 +1578,7 @@ Zotero.Translate.Base.prototype = {
|
||||||
|
|
||||||
// Trigger itemDone events
|
// Trigger itemDone events
|
||||||
for(var i=0, nItems = items.length; i<nItems; i++) {
|
for(var i=0, nItems = items.length; i<nItems; i++) {
|
||||||
me._runHandler("itemDone", newItems[i], items[i]);
|
this._runHandler("itemDone", newItems[i], items[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Specify that itemDone event was dispatched, so that we don't defer
|
// Specify that itemDone event was dispatched, so that we don't defer
|
||||||
|
@ -1570,39 +1587,18 @@ Zotero.Translate.Base.prototype = {
|
||||||
|
|
||||||
// Run deferred attachmentProgress notifications
|
// Run deferred attachmentProgress notifications
|
||||||
for(var i=0; i<deferredProgress.length; i++) {
|
for(var i=0; i<deferredProgress.length; i++) {
|
||||||
me._runHandler("attachmentProgress", deferredProgress[i][0],
|
this._runHandler("attachmentProgress", deferredProgress[i][0],
|
||||||
deferredProgress[i][1], deferredProgress[i][2]);
|
deferredProgress[i][1], deferredProgress[i][2]);
|
||||||
}
|
}
|
||||||
|
|
||||||
me.newItems = me.newItems.concat(newItems);
|
this.newItems = this.newItems.concat(newItems);
|
||||||
me._savingItems--;
|
this._savingItems--;
|
||||||
me._checkIfDone();
|
this._checkIfDone();
|
||||||
} else {
|
}.bind(this))
|
||||||
Zotero.logError(newItems);
|
.catch(function (e) {
|
||||||
me.complete(returnValue, newItems);
|
Zotero.logError(e);
|
||||||
}
|
this.complete(false, e);
|
||||||
},
|
}.bind(this));
|
||||||
function(attachment, progress, error) {
|
|
||||||
var attachmentIndex = me._savingAttachments.indexOf(attachment);
|
|
||||||
if(progress === false || progress === 100) {
|
|
||||||
if(attachmentIndex !== -1) {
|
|
||||||
me._savingAttachments.splice(attachmentIndex, 1);
|
|
||||||
}
|
|
||||||
} else if(attachmentIndex === -1) {
|
|
||||||
me._savingAttachments.push(attachment);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(itemDoneEventsDispatched) {
|
|
||||||
// itemDone event has already fired, so we can fire attachmentProgress
|
|
||||||
// notifications
|
|
||||||
me._runHandler("attachmentProgress", attachment, progress, error);
|
|
||||||
me._checkIfDone();
|
|
||||||
} else {
|
|
||||||
// Defer until after we fire the itemDone event
|
|
||||||
deferredProgress.push([attachment, progress, error]);
|
|
||||||
attachmentsWithProgress.push(attachment);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -74,17 +74,12 @@ Zotero.Translate.ItemSaver.prototype = {
|
||||||
/**
|
/**
|
||||||
* Saves items to Standalone or the server
|
* Saves items to Standalone or the server
|
||||||
* @param items Items in Zotero.Item.toArray() format
|
* @param items Items in Zotero.Item.toArray() format
|
||||||
* @param {Function} callback A callback to be executed when saving is complete. If saving
|
|
||||||
* succeeded, this callback will be passed true as the first argument and a list of items
|
|
||||||
* saved as the second. If saving failed, the callback will be passed false as the first
|
|
||||||
* argument and an error object as the second
|
|
||||||
* @param {Function} [attachmentCallback] A callback that receives information about attachment
|
* @param {Function} [attachmentCallback] A callback that receives information about attachment
|
||||||
* save progress. The callback will be called as attachmentCallback(attachment, false, error)
|
* save progress. The callback will be called as attachmentCallback(attachment, false, error)
|
||||||
* on failure or attachmentCallback(attachment, progressPercent) periodically during saving.
|
* on failure or attachmentCallback(attachment, progressPercent) periodically during saving.
|
||||||
*/
|
*/
|
||||||
"saveItems": Zotero.Promise.coroutine(function* (items, callback, attachmentCallback) {
|
saveItems: Zotero.Promise.coroutine(function* (items, attachmentCallback) {
|
||||||
try {
|
let newItems = [], standaloneAttachments = [], childAttachments = [];
|
||||||
let newItems = [], standaloneAttachments = [];
|
|
||||||
yield Zotero.DB.executeTransaction(function* () {
|
yield Zotero.DB.executeTransaction(function* () {
|
||||||
for (let iitem=0; iitem<items.length; iitem++) {
|
for (let iitem=0; iitem<items.length; iitem++) {
|
||||||
let item = items[iitem], newItem, myID;
|
let item = items[iitem], newItem, myID;
|
||||||
|
@ -93,8 +88,11 @@ Zotero.Translate.ItemSaver.prototype = {
|
||||||
|
|
||||||
if (type == "note") { // handle notes differently
|
if (type == "note") { // handle notes differently
|
||||||
newItem = yield this._saveNote(item);
|
newItem = yield this._saveNote(item);
|
||||||
} else if (type == "attachment") { // handle attachments differently
|
}
|
||||||
standaloneAttachments.push(iitem);
|
// Handle standalone attachments differently
|
||||||
|
else if (type == "attachment") {
|
||||||
|
standaloneAttachments.push(items[iitem]);
|
||||||
|
attachmentCallback(items[iitem], 0);
|
||||||
continue;
|
continue;
|
||||||
} else {
|
} else {
|
||||||
newItem = new Zotero.Item(type);
|
newItem = new Zotero.Item(type);
|
||||||
|
@ -127,11 +125,9 @@ Zotero.Translate.ItemSaver.prototype = {
|
||||||
|
|
||||||
// handle attachments
|
// handle attachments
|
||||||
if (specialFields.attachments) {
|
if (specialFields.attachments) {
|
||||||
for (let i=0; i<specialFields.attachments.length; i++) {
|
for (let attachment of specialFields.attachments) {
|
||||||
let attachment = specialFields.attachments[i];
|
childAttachments.push([attachment, myID]);
|
||||||
// Don't wait for the promise to resolve, since we want to
|
attachmentCallback(attachment, 0);
|
||||||
// signal completion as soon as the items are saved
|
|
||||||
this._saveAttachment(attachment, myID, attachmentCallback);
|
|
||||||
}
|
}
|
||||||
// Restore the attachments field, since we use it later in
|
// Restore the attachments field, since we use it later in
|
||||||
// translation
|
// translation
|
||||||
|
@ -147,21 +143,29 @@ Zotero.Translate.ItemSaver.prototype = {
|
||||||
}
|
}
|
||||||
}.bind(this));
|
}.bind(this));
|
||||||
|
|
||||||
// Handle standalone attachments outside of the transaction
|
// Handle standalone attachments outside of the transaction, because they can involve downloading
|
||||||
for (let iitem of standaloneAttachments) {
|
for (let item of standaloneAttachments) {
|
||||||
let newItem = yield this._saveAttachment(items[iitem], null, attachmentCallback);
|
let newItem = yield this._saveAttachment(item, null, attachmentCallback);
|
||||||
if (newItem) newItems.push(newItem);
|
if (newItem) newItems.push(newItem);
|
||||||
}
|
}
|
||||||
|
// Save attachments afterwards, since we want to signal completion as soon as the main items
|
||||||
callback(true, newItems);
|
// are saved
|
||||||
} catch(e) {
|
var promise = Zotero.Promise.delay(1);
|
||||||
callback(false, e);
|
for (let a of childAttachments) {
|
||||||
|
// Workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=449811 (fixed in Fx51?)
|
||||||
|
let [item, parentItemID] = a;
|
||||||
|
promise = promise.then(() => this._saveAttachment(item, parentItemID, attachmentCallback));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return newItems;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
"saveCollections": Zotero.Promise.coroutine(function* (collections) {
|
"saveCollections": Zotero.Promise.coroutine(function* (collections) {
|
||||||
var collectionsToProcess = collections.slice();
|
var collectionsToProcess = collections.slice();
|
||||||
var parentIDs = [null];
|
// Use first collection passed to translate process as the root
|
||||||
|
var rootCollectionID = (this._collections && this._collections.length)
|
||||||
|
? this._collections[0] : null;
|
||||||
|
var parentIDs = collections.map(c => null);
|
||||||
var topLevelCollections = [];
|
var topLevelCollections = [];
|
||||||
|
|
||||||
yield Zotero.DB.executeTransaction(function* () {
|
yield Zotero.DB.executeTransaction(function* () {
|
||||||
|
@ -175,9 +179,13 @@ Zotero.Translate.ItemSaver.prototype = {
|
||||||
if (parentID) {
|
if (parentID) {
|
||||||
newCollection.parentID = parentID;
|
newCollection.parentID = parentID;
|
||||||
}
|
}
|
||||||
yield newCollection.save();
|
else {
|
||||||
|
newCollection.parentID = rootCollectionID;
|
||||||
if(parentID === null) topLevelCollections.push(newCollection);
|
topLevelCollections.push(newCollection)
|
||||||
|
}
|
||||||
|
yield newCollection.save({
|
||||||
|
skipSelect: true
|
||||||
|
});
|
||||||
|
|
||||||
var toAdd = [];
|
var toAdd = [];
|
||||||
|
|
||||||
|
@ -222,7 +230,7 @@ Zotero.Translate.ItemSaver.prototype = {
|
||||||
* Saves a translator attachment to the database
|
* Saves a translator attachment to the database
|
||||||
*
|
*
|
||||||
* @param {Translator Attachment} attachment
|
* @param {Translator Attachment} attachment
|
||||||
* @param {Integer} parentID Item to attach to
|
* @param {Integer} parentItemID - Item to attach to
|
||||||
* @param {Function} attachmentCallback Callback function that takes three
|
* @param {Function} attachmentCallback Callback function that takes three
|
||||||
* parameters: translator attachment object, percent completion (integer),
|
* parameters: translator attachment object, percent completion (integer),
|
||||||
* and an optional error object
|
* and an optional error object
|
||||||
|
@ -230,7 +238,7 @@ Zotero.Translate.ItemSaver.prototype = {
|
||||||
* @return {Zotero.Primise<Zotero.Item|False} Flase is returned if attachment
|
* @return {Zotero.Primise<Zotero.Item|False} Flase is returned if attachment
|
||||||
* was not saved due to error or user settings.
|
* was not saved due to error or user settings.
|
||||||
*/
|
*/
|
||||||
"_saveAttachment": Zotero.Promise.coroutine(function* (attachment, parentID, attachmentCallback) {
|
_saveAttachment: Zotero.Promise.coroutine(function* (attachment, parentItemID, attachmentCallback) {
|
||||||
try {
|
try {
|
||||||
let newAttachment;
|
let newAttachment;
|
||||||
|
|
||||||
|
@ -263,7 +271,7 @@ Zotero.Translate.ItemSaver.prototype = {
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
"_saveAttachmentFile": Zotero.Promise.coroutine(function* (attachment, parentID, attachmentCallback) {
|
_saveAttachmentFile: Zotero.Promise.coroutine(function* (attachment, parentItemID, attachmentCallback) {
|
||||||
Zotero.debug("Translate: Adding attachment", 4);
|
Zotero.debug("Translate: Adding attachment", 4);
|
||||||
attachmentCallback(attachment, 0);
|
attachmentCallback(attachment, 0);
|
||||||
|
|
||||||
|
@ -319,9 +327,10 @@ Zotero.Translate.ItemSaver.prototype = {
|
||||||
attachment.linkMode = "linked_file";
|
attachment.linkMode = "linked_file";
|
||||||
newItem = yield Zotero.Attachments.linkFromURL({
|
newItem = yield Zotero.Attachments.linkFromURL({
|
||||||
url: attachment.url,
|
url: attachment.url,
|
||||||
parentItemID: parentID,
|
parentItemID,
|
||||||
contentType: attachment.mimeType || undefined,
|
contentType: attachment.mimeType || undefined,
|
||||||
title: attachment.title || undefined
|
title: attachment.title || undefined,
|
||||||
|
collections: !parentItemID ? this._collections : undefined
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
if (attachment.url) {
|
if (attachment.url) {
|
||||||
|
@ -332,14 +341,16 @@ Zotero.Translate.ItemSaver.prototype = {
|
||||||
title: attachment.title,
|
title: attachment.title,
|
||||||
contentType: attachment.mimeType,
|
contentType: attachment.mimeType,
|
||||||
charset: attachment.charset,
|
charset: attachment.charset,
|
||||||
parentItemID: parentID
|
parentItemID,
|
||||||
|
collections: !parentItemID ? this._collections : undefined
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
attachment.linkMode = "imported_file";
|
attachment.linkMode = "imported_file";
|
||||||
newItem = yield Zotero.Attachments.importFromFile({
|
newItem = yield Zotero.Attachments.importFromFile({
|
||||||
file: file,
|
file: file,
|
||||||
parentItemID: parentID
|
parentItemID,
|
||||||
|
collections: !parentItemID ? this._collections : undefined
|
||||||
});
|
});
|
||||||
if (attachment.title) newItem.setField("title", attachment.title);
|
if (attachment.title) newItem.setField("title", attachment.title);
|
||||||
}
|
}
|
||||||
|
@ -473,7 +484,7 @@ Zotero.Translate.ItemSaver.prototype = {
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
"_saveAttachmentDownload": Zotero.Promise.coroutine(function* (attachment, parentID, attachmentCallback) {
|
_saveAttachmentDownload: Zotero.Promise.coroutine(function* (attachment, parentItemID, attachmentCallback) {
|
||||||
Zotero.debug("Translate: Adding attachment", 4);
|
Zotero.debug("Translate: Adding attachment", 4);
|
||||||
|
|
||||||
if(!attachment.url && !attachment.document) {
|
if(!attachment.url && !attachment.document) {
|
||||||
|
@ -542,9 +553,10 @@ Zotero.Translate.ItemSaver.prototype = {
|
||||||
|
|
||||||
return Zotero.Attachments.linkFromURL({
|
return Zotero.Attachments.linkFromURL({
|
||||||
url: cleanURI,
|
url: cleanURI,
|
||||||
parentItemID: parentID,
|
parentItemID,
|
||||||
contentType: mimeType,
|
contentType: mimeType,
|
||||||
title: title
|
title,
|
||||||
|
collections: !parentItemID ? this._collections : undefined
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -558,16 +570,17 @@ Zotero.Translate.ItemSaver.prototype = {
|
||||||
return Zotero.Attachments.importFromDocument({
|
return Zotero.Attachments.importFromDocument({
|
||||||
libraryID: this._libraryID,
|
libraryID: this._libraryID,
|
||||||
document: attachment.document,
|
document: attachment.document,
|
||||||
parentItemID: parentID,
|
parentItemID,
|
||||||
title: title
|
title,
|
||||||
|
collections: !parentItemID ? this._collections : undefined
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Import from URL
|
// Import from URL
|
||||||
let mimeType = attachment.mimeType ? attachment.mimeType : null;
|
let mimeType = attachment.mimeType ? attachment.mimeType : null;
|
||||||
let fileBaseName;
|
let fileBaseName;
|
||||||
if (parentID) {
|
if (parentItemID) {
|
||||||
let parentItem = yield Zotero.Items.getAsync(parentID);
|
let parentItem = yield Zotero.Items.getAsync(parentItemID);
|
||||||
fileBaseName = Zotero.Attachments.getFileBaseNameFromItem(parentItem);
|
fileBaseName = Zotero.Attachments.getFileBaseNameFromItem(parentItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -579,19 +592,20 @@ Zotero.Translate.ItemSaver.prototype = {
|
||||||
return Zotero.Attachments.importFromURL({
|
return Zotero.Attachments.importFromURL({
|
||||||
libraryID: this._libraryID,
|
libraryID: this._libraryID,
|
||||||
url: attachment.url,
|
url: attachment.url,
|
||||||
parentItemID: parentID,
|
parentItemID,
|
||||||
title: title,
|
title,
|
||||||
fileBaseName: fileBaseName,
|
fileBaseName,
|
||||||
contentType: mimeType,
|
contentType: mimeType,
|
||||||
cookieSandbox: this._cookieSandbox
|
cookieSandbox: this._cookieSandbox,
|
||||||
|
collections: !parentItemID ? this._collections : undefined
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
|
|
||||||
"_saveNote":Zotero.Promise.coroutine(function* (note, parentID) {
|
"_saveNote":Zotero.Promise.coroutine(function* (note, parentItemID) {
|
||||||
var myNote = new Zotero.Item('note');
|
var myNote = new Zotero.Item('note');
|
||||||
myNote.libraryID = this._libraryID;
|
myNote.libraryID = this._libraryID;
|
||||||
if(parentID) {
|
if (parentItemID) {
|
||||||
myNote.parentID = parentID;
|
myNote.parentItemID = parentItemID;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(typeof note == "object") {
|
if(typeof note == "object") {
|
||||||
|
@ -601,7 +615,7 @@ Zotero.Translate.ItemSaver.prototype = {
|
||||||
} else {
|
} else {
|
||||||
myNote.setNote(note);
|
myNote.setNote(note);
|
||||||
}
|
}
|
||||||
if (!parentID && this._collections) {
|
if (!parentItemID && this._collections) {
|
||||||
myNote.setCollections(this._collections);
|
myNote.setCollections(this._collections);
|
||||||
}
|
}
|
||||||
yield myNote.save();
|
yield myNote.save();
|
||||||
|
|
|
@ -649,7 +649,8 @@ zotero.preferences.advanced.debug.error = An error occurred sending debug output
|
||||||
dragAndDrop.existingFiles = The following files already existed in the destination directory and were not copied:
|
dragAndDrop.existingFiles = The following files already existed in the destination directory and were not copied:
|
||||||
dragAndDrop.filesNotFound = The following files were not found and could not be copied:
|
dragAndDrop.filesNotFound = The following files were not found and could not be copied:
|
||||||
|
|
||||||
fileInterface.itemsImported = Importing items…
|
fileInterface.importComplete = Import Complete
|
||||||
|
fileInterface.itemsWereImported = %1$S item was imported;%1$S items were imported
|
||||||
fileInterface.itemsExported = Exporting items…
|
fileInterface.itemsExported = Exporting items…
|
||||||
fileInterface.import = Import
|
fileInterface.import = Import
|
||||||
fileInterface.export = Export
|
fileInterface.export = Export
|
||||||
|
|
|
@ -596,6 +596,92 @@ describe("Zotero.Translate", function() {
|
||||||
Zotero.Translators.get.restore();
|
Zotero.Translators.get.restore();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("ItemSaver", function () {
|
||||||
|
describe("#saveCollections()", function () {
|
||||||
|
it("should add top-level collections to specified collection", function* () {
|
||||||
|
var collection = yield createDataObject('collection');
|
||||||
|
var collections = [
|
||||||
|
{
|
||||||
|
name: "Collection",
|
||||||
|
type: "collection",
|
||||||
|
children: []
|
||||||
|
}
|
||||||
|
];
|
||||||
|
var items = [
|
||||||
|
{
|
||||||
|
itemType: "book",
|
||||||
|
title: "Test"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
var translation = new Zotero.Translate.Import();
|
||||||
|
translation.setString("");
|
||||||
|
translation.setTranslator(buildDummyTranslator(
|
||||||
|
"import",
|
||||||
|
"function detectImport() {}\n"
|
||||||
|
+ "function doImport() {\n"
|
||||||
|
+ " var json = JSON.parse('" + JSON.stringify(collections).replace(/['\\]/g, "\\$&") + "');\n"
|
||||||
|
+ " for (let o of json) {"
|
||||||
|
+ " var collection = new Zotero.Collection;\n"
|
||||||
|
+ " for (let field in o) { collection[field] = o[field]; }\n"
|
||||||
|
+ " collection.complete();\n"
|
||||||
|
+ " }\n"
|
||||||
|
+ " json = JSON.parse('" + JSON.stringify(items).replace(/['\\]/g, "\\$&") + "');\n"
|
||||||
|
+ " for (let o of json) {"
|
||||||
|
+ " var item = new Zotero.Item;\n"
|
||||||
|
+ " for (let field in o) { item[field] = o[field]; }\n"
|
||||||
|
+ " item.complete();\n"
|
||||||
|
+ " }\n"
|
||||||
|
+ "}"
|
||||||
|
));
|
||||||
|
yield translation.translate({
|
||||||
|
collections: [collection.id]
|
||||||
|
});
|
||||||
|
assert.lengthOf(translation.newCollections, 1);
|
||||||
|
assert.isNumber(translation.newCollections[0].id);
|
||||||
|
assert.lengthOf(translation.newItems, 1);
|
||||||
|
assert.isNumber(translation.newItems[0].id);
|
||||||
|
var childCollections = Array.from(collection.getChildCollections(true));
|
||||||
|
assert.sameMembers(childCollections, translation.newCollections.map(c => c.id));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("#_saveAttachment()", function () {
|
||||||
|
it("should save standalone attachment to collection", function* () {
|
||||||
|
var collection = yield createDataObject('collection');
|
||||||
|
var items = [
|
||||||
|
{
|
||||||
|
itemType: "attachment",
|
||||||
|
title: "Test",
|
||||||
|
mimeType: "text/html",
|
||||||
|
url: "http://example.com"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
var translation = new Zotero.Translate.Import();
|
||||||
|
translation.setString("");
|
||||||
|
translation.setTranslator(buildDummyTranslator(
|
||||||
|
"import",
|
||||||
|
"function detectImport() {}\n"
|
||||||
|
+ "function doImport() {\n"
|
||||||
|
+ " var json = JSON.parse('" + JSON.stringify(items).replace(/['\\]/g, "\\$&") + "');\n"
|
||||||
|
+ " for (var i=0; i<json.length; i++) {"
|
||||||
|
+ " var item = new Zotero.Item;\n"
|
||||||
|
+ " for (var field in json[i]) { item[field] = json[i][field]; }\n"
|
||||||
|
+ " item.complete();\n"
|
||||||
|
+ " }\n"
|
||||||
|
+ "}"
|
||||||
|
));
|
||||||
|
yield translation.translate({
|
||||||
|
collections: [collection.id]
|
||||||
|
});
|
||||||
|
assert.lengthOf(translation.newItems, 1);
|
||||||
|
assert.isNumber(translation.newItems[0].id);
|
||||||
|
assert.ok(collection.hasItem(translation.newItems[0].id));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Zotero.Translate.ItemGetter", function() {
|
describe("Zotero.Translate.ItemGetter", function() {
|
||||||
|
|
Loading…
Add table
Reference in a new issue