diff --git a/chrome/content/zotero/collectionTree.jsx b/chrome/content/zotero/collectionTree.jsx index 0708de1cf2..da1753955c 100644 --- a/chrome/content/zotero/collectionTree.jsx +++ b/chrome/content/zotero/collectionTree.jsx @@ -2045,159 +2045,11 @@ var CollectionTree = class CollectionTree extends LibraryTree { return true; } - /** - * Copy a given item into another library. Used when we need to create a copy of a collection - * in another library if collection is drag-dropped into a group it is not a part of. - */ - async _copyItem({ item, targetLibraryID, targetTreeRow, options }) { - // Check if there's already a copy of this item in the library - var linkedItem = await item.getLinkedItem(targetLibraryID, true); - if (linkedItem) { - return linkedItem.id; - - /* - // TODO: support tags, related, attachments, etc. - - // Overlay source item fields on unsaved clone of linked item - var newItem = item.clone(false, linkedItem.clone(true)); - newItem.setField('dateAdded', item.dateAdded); - newItem.setField('dateModified', item.dateModified); - - var diff = newItem.diff(linkedItem, false, ["dateAdded", "dateModified"]); - if (!diff) { - // Check if creators changed - var creatorsChanged = false; - - var creators = item.getCreators(); - var linkedCreators = linkedItem.getCreators(); - if (creators.length != linkedCreators.length) { - Zotero.debug('Creators have changed'); - creatorsChanged = true; - } - else { - for (var i=0; i { var collections = [{ id: collection.id, @@ -2291,7 +2139,6 @@ var CollectionTree = class CollectionTree extends LibraryTree { parentID: targetCollectionID, addItems, targetLibraryID, - targetTreeRow, copyOptions }); for (let [collectionID, items] of addItems.entries()) { @@ -2388,7 +2235,6 @@ var CollectionTree = class CollectionTree extends LibraryTree { collection: droppedCollection, targetCollectionID, targetLibraryID, - targetTreeRow, copyOptions }); } @@ -2507,12 +2353,10 @@ var CollectionTree = class CollectionTree extends LibraryTree { return Zotero.DB.executeTransaction(async () => { let copiedItemIDs = []; for (let item of chunk) { - var id = await this._copyItem({ - item, - targetLibraryID, - targetTreeRow, - options: copyOptions - }); + let copiedItem = await Zotero.Items.copyToLibrary( + item, targetLibraryID, copyOptions + ); + let id = copiedItem && copiedItem.id; // Standalone attachments might not get copied if (!id) { continue; diff --git a/chrome/content/zotero/xpcom/data/items.js b/chrome/content/zotero/xpcom/data/items.js index dd35b89a53..6cde975a68 100644 --- a/chrome/content/zotero/xpcom/data/items.js +++ b/chrome/content/zotero/xpcom/data/items.js @@ -967,6 +967,128 @@ Zotero.Items = function () { }; + /** + * Copy an item to another library, including enabled child items + * + * Requires a transaction + * + * @param {Zotero.Item} item + * @param {Integer} targetLibraryID + * @param {Object} [options] + * @param {Boolean} [options.tags] + * @param {Boolean} [options.childNotes] + * @param {Boolean} [options.childLinks] + * @param {Boolean} [options.childFileAttachments] + * @param {Boolean} [options.annotations] + * @return {Promise} The copied or existing linked item, or false if an + * attachment was skipped + */ + this.copyToLibrary = async function (item, targetLibraryID, options = {}) { + Zotero.DB.requireTransaction(); + + if (!item.isRegularItem() && !item.isNote() && !item.isAttachment()) { + throw new Error("Only regular items, notes, and attachments can be copied between libraries"); + } + + // Check if there's already a copy of this item in the library + let linkedItem = await item.getLinkedItem(targetLibraryID, true); + if (linkedItem) { + // TODO: Support reconciling tags, related items, attachments, etc. + return linkedItem; + } + + let targetLibrary = Zotero.Libraries.get(targetLibraryID); + let { + tags: copyTags = Zotero.Prefs.get('groups.copyTags'), + childNotes: copyChildNotes = Zotero.Prefs.get('groups.copyChildNotes'), + childLinks: copyChildLinks = Zotero.Prefs.get('groups.copyChildLinks'), + childFileAttachments: copyChildFileAttachments + = Zotero.Prefs.get('groups.copyChildFileAttachments'), + annotations: copyAnnotations = Zotero.Prefs.get('groups.copyAnnotations'), + } = options; + + // Standalone attachment + if (item.isAttachment()) { + // Skip linked files + if (item.attachmentLinkMode == Zotero.Attachments.LINK_MODE_LINKED_FILE) { + Zotero.debug("Skipping standalone linked file attachment on copy"); + return false; + } + if (!targetLibrary.filesEditable) { + Zotero.debug("Skipping standalone file attachment on copy"); + return false; + } + let newAttachment = await Zotero.Attachments.copyAttachmentToLibrary(item, targetLibraryID); + if (copyAnnotations) { + await this.copyChildItems(item, newAttachment); + } + return newAttachment; + } + + // Create new clone item in target library + let newItem = item.clone(targetLibraryID, { skipTags: !copyTags }); + await newItem.save({ skipSelect: true }); + + // Record link + await newItem.addLinkedItem(item); + + if (item.isNote()) { + if (targetLibrary.filesEditable) { + await Zotero.Notes.copyEmbeddedImages(item, newItem); + } + return newItem; + } + + // For regular items, add child items if prefs and permissions allow + + // Child notes + if (copyChildNotes) { + for (let note of Zotero.Items.get(item.getNotes())) { + let newNote = note.clone(targetLibraryID, { skipTags: !copyTags }); + newNote.parentID = newItem.id; + await newNote.save({ skipSelect: true }); + if (targetLibrary.filesEditable) { + await Zotero.Notes.copyEmbeddedImages(note, newNote); + } + await newNote.addLinkedItem(note); + } + } + + // Child attachments + if (copyChildLinks || copyChildFileAttachments) { + for (let attachment of Zotero.Items.get(item.getAttachments())) { + let linkMode = attachment.attachmentLinkMode; + + // Skip linked files + if (linkMode == Zotero.Attachments.LINK_MODE_LINKED_FILE) { + Zotero.debug("Skipping child linked file attachment on copy"); + continue; + } + + // Skip imported files if we don't have pref and permissions + if (linkMode == Zotero.Attachments.LINK_MODE_LINKED_URL) { + if (!copyChildLinks) { + Zotero.debug("Skipping child link attachment on copy"); + continue; + } + } + else if (!copyChildFileAttachments || !targetLibrary.filesEditable) { + Zotero.debug("Skipping child file attachment on copy"); + continue; + } + let newAttachment = await Zotero.Attachments.copyAttachmentToLibrary( + attachment, targetLibraryID, newItem.id + ); + if (copyAnnotations) { + await this.copyChildItems(attachment, newAttachment); + } + } + } + + return newItem; + }; + + /** * Copy child items from one item to another (e.g., in another library) * diff --git a/test/tests/itemsTest.js b/test/tests/itemsTest.js index 9842f2bd60..bedfe4a9df 100644 --- a/test/tests/itemsTest.js +++ b/test/tests/itemsTest.js @@ -153,6 +153,73 @@ describe("Zotero.Items", function () { }); + describe("#copyToLibrary()", function () { + var group; + var copyChildNotes; + + before(async function () { + group = await createGroup(); + }); + + beforeEach(function () { + copyChildNotes = Zotero.Prefs.get('groups.copyChildNotes'); + Zotero.Prefs.set('groups.copyChildNotes', true); + }); + + afterEach(function () { + Zotero.Prefs.set('groups.copyChildNotes', copyChildNotes); + }); + + after(async function () { + await group.eraseTx(); + }); + + it("should copy and link a regular item into a group and reuse it", async function () { + var item = await createDataObject('item'); + var newItem; + await Zotero.DB.executeTransaction(async function () { + newItem = await Zotero.Items.copyToLibrary(item, group.libraryID); + }); + + assert.instanceOf(newItem, Zotero.Item); + assert.equal(newItem.libraryID, group.libraryID); + assert.equal(await item.getLinkedItem(group.libraryID, true), newItem); + + var reusedItem; + await Zotero.DB.executeTransaction(async function () { + reusedItem = await Zotero.Items.copyToLibrary(item, group.libraryID); + }); + assert.strictEqual(reusedItem, newItem); + }); + + it("should copy a child note", async function () { + var item = await createDataObject('item'); + var note = await createDataObject('item', { itemType: 'note', parentID: item.id }); + var newItem; + await Zotero.DB.executeTransaction(async function () { + newItem = await Zotero.Items.copyToLibrary(item, group.libraryID); + }); + + var notes = Zotero.Items.get(newItem.getNotes()); + assert.lengthOf(notes, 1); + assert.equal(await note.getLinkedItem(group.libraryID, true), notes[0]); + }); + + it("should honor disabled child-note copying", async function () { + var item = await createDataObject('item'); + await createDataObject('item', { itemType: 'note', parentID: item.id }); + var newItem; + await Zotero.DB.executeTransaction(async function () { + newItem = await Zotero.Items.copyToLibrary(item, group.libraryID, { + childNotes: false, + }); + }); + + assert.isEmpty(newItem.getNotes()); + }); + }); + + describe("#copyChildItems()", function () { var group;