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:
Dan Stillman 2016-12-09 04:36:42 -05:00
parent c61a9dc5f3
commit 78b1d2ee35
7 changed files with 327 additions and 244 deletions

View file

@ -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();
}); });
/* /*

View file

@ -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;
}, },
/** /**

View file

@ -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;
}) })
} }

View file

@ -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);
}
});
}, },
/** /**

View file

@ -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();

View file

@ -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

View file

@ -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() {