www

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | Submodules | README | LICENSE

commit a949d6bf8df4c99fa313db57bdd8cb51649e6292
parent de897d287832fbd85fe0f54e13125c69cd77e86c
Author: Dan Stillman <dstillman@zotero.org>
Date:   Wed, 16 Mar 2016 02:02:41 -0400

Merge branch 'deasyncification'

Diffstat:
Mchrome/content/zotero/bindings/itembox.xml | 2+-
Mchrome/content/zotero/bindings/noteeditor.xml | 61+++++++++++++++++++++++++++----------------------------------
Mchrome/content/zotero/bindings/relatedbox.xml | 182++++++++++++++++++++++++++++++++++++-------------------------------------------
Mchrome/content/zotero/bindings/tagsbox.xml | 97+++++++++++++++++++++++++++++++++++--------------------------------------------
Mchrome/content/zotero/bindings/tagselector.xml | 98++++++++++++++++++++++++++++++++++++-------------------------------------------
Mchrome/content/zotero/duplicatesMerge.js | 51+++++++++++++++++++++++++--------------------------
Mchrome/content/zotero/fileInterface.js | 17+++++++++--------
Mchrome/content/zotero/itemPane.js | 3---
Mchrome/content/zotero/locateMenu.js | 1-
Mchrome/content/zotero/recognizePDF.js | 1-
Mchrome/content/zotero/xpcom/api.js | 1-
Mchrome/content/zotero/xpcom/attachments.js | 3+--
Mchrome/content/zotero/xpcom/cite.js | 98+++++++++++++++++++++++++++++++------------------------------------------------
Mchrome/content/zotero/xpcom/collectionTreeRow.js | 1+
Mchrome/content/zotero/xpcom/collectionTreeView.js | 74++++++++++++++++++++++++++++++++++++--------------------------------------
Mchrome/content/zotero/xpcom/data/collection.js | 169+++++++++++++-------------------------------------------------------------------
Mchrome/content/zotero/xpcom/data/collections.js | 100+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mchrome/content/zotero/xpcom/data/creators.js | 40++++++++++++++++++++++------------------
Mchrome/content/zotero/xpcom/data/dataObject.js | 125+++++++++++++------------------------------------------------------------------
Mchrome/content/zotero/xpcom/data/dataObjects.js | 274+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Mchrome/content/zotero/xpcom/data/item.js | 705++++++++++++++++++++++++++++++-------------------------------------------------
Mchrome/content/zotero/xpcom/data/items.js | 625++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
Mchrome/content/zotero/xpcom/data/library.js | 12++++++++++++
Mchrome/content/zotero/xpcom/data/tags.js | 45+++++++++++++++++++++------------------------
Mchrome/content/zotero/xpcom/date.js | 8++++++--
Mchrome/content/zotero/xpcom/db.js | 8+++++++-
Mchrome/content/zotero/xpcom/duplicates.js | 12++++++------
Mchrome/content/zotero/xpcom/itemTreeView.js | 183+++++++++++++++++++++++++++++--------------------------------------------------
Mchrome/content/zotero/xpcom/libraryTreeView.js | 4++--
Mchrome/content/zotero/xpcom/schema.js | 2+-
Mchrome/content/zotero/xpcom/search.js | 192+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Mchrome/content/zotero/xpcom/storage.js | 4++--
Mchrome/content/zotero/xpcom/storage/storageEngine.js | 4++--
Mchrome/content/zotero/xpcom/storage/storageLocal.js | 155++++++++++++++-----------------------------------------------------------------
Mchrome/content/zotero/xpcom/storage/streamListener.js | 2++
Mchrome/content/zotero/xpcom/storage/webdav.js | 71+++++++++++++++++++++++++++++++++++++----------------------------------
Mchrome/content/zotero/xpcom/storage/zfs.js | 54+++++++++++++++++++++++++++++-------------------------
Mchrome/content/zotero/xpcom/style.js | 7+++----
Mchrome/content/zotero/xpcom/sync/syncEngine.js | 4++--
Mchrome/content/zotero/xpcom/sync/syncLocal.js | 6+++---
Mchrome/content/zotero/xpcom/syncedSettings.js | 126++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Mchrome/content/zotero/xpcom/timeline.js | 1-
Mchrome/content/zotero/xpcom/translation/translate_item.js | 8+++-----
Mchrome/content/zotero/xpcom/utilities.js | 5+++--
Mchrome/content/zotero/xpcom/utilities_internal.js | 72++++++++++++++++++++++++++++++++++--------------------------------------
Mchrome/content/zotero/xpcom/zotero.js | 11+++++++++--
Mchrome/content/zotero/zoteroPane.js | 53++++++++++++++++++++---------------------------------
Mcomponents/zotero-protocol-handler.js | 18++++++++----------
Mtest/content/runtests.js | 2++
Mtest/content/support.js | 34++++++++++++++++++++++++++++++----
Mtest/tests/collectionTest.js | 6------
Mtest/tests/collectionTreeViewTest.js | 62+++++++++++++++++++++++++++++++++++++++-----------------------
Atest/tests/creatorsTest.js | 21+++++++++++++++++++++
Mtest/tests/dataObjectTest.js | 4+---
Mtest/tests/dataObjectUtilitiesTest.js | 4++--
Mtest/tests/dateTest.js | 29+++++++++++++++++++++++++++++
Atest/tests/duplicatesTest.js | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtest/tests/fileInterfaceTest.js | 2+-
Mtest/tests/itemTest.js | 145+++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
Mtest/tests/itemTreeViewTest.js | 37++++++++++++++++++++++++++++++++-----
Mtest/tests/libraryTest.js | 5+++++
Mtest/tests/preferences_syncTest.js | 2+-
Mtest/tests/relatedboxTest.js | 6++----
Mtest/tests/searchTest.js | 12++++++------
Mtest/tests/storageLocalTest.js | 72++++++++++++++++++++++++++++++------------------------------------------
Mtest/tests/syncEngineTest.js | 28+++++++++++++---------------
Mtest/tests/syncLocalTest.js | 67+++++++++++++++++++++++--------------------------------------------
Mtest/tests/tagSelectorTest.js | 14+++++++++++++-
Mtest/tests/tagsTest.js | 34++++++++++++++++++++++++++++++++++
Mtest/tests/tagsboxTest.js | 14--------------
Mtest/tests/translateTest.js | 16++++++++--------
Mtest/tests/utilitiesTest.js | 27++++++++++++++-------------
Mtest/tests/webdavTest.js | 38+++++++++++++++-----------------------
Mtest/tests/zfsTest.js | 95+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
Mtest/tests/zoteroPaneTest.js | 93+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
75 files changed, 2608 insertions(+), 2112 deletions(-)

diff --git a/chrome/content/zotero/bindings/itembox.xml b/chrome/content/zotero/bindings/itembox.xml @@ -1569,7 +1569,7 @@ this._lastTabIndex = parseInt(textbox.getAttribute('ztabindex')) - 1; this._tabDirection = 1; - var creator = yield Zotero.Creators.getAsync(creatorID); + var creator = Zotero.Creators.get(creatorID); var otherField = creatorField == 'lastName' ? 'firstName' : 'lastName'; diff --git a/chrome/content/zotero/bindings/noteeditor.xml b/chrome/content/zotero/bindings/noteeditor.xml @@ -417,48 +417,41 @@ </method> <method name="updateTagsSummary"> <body><![CDATA[ - Zotero.spawn(function* () { - var v = yield this.id('tags').summary; - - if (!v || v == "") { - v = "[" + Zotero.getString('pane.item.noteEditor.clickHere') + "]"; - } - - this.id('tagsLabel').value = Zotero.getString('itemFields.tags') - + Zotero.getString('punctuation.colon'); - this.id('tagsClick').value = v; - }, this); + var v = this.id('tags').summary; + + if (!v || v == "") { + v = "[" + Zotero.getString('pane.item.noteEditor.clickHere') + "]"; + } + + this.id('tagsLabel').value = Zotero.getString('itemFields.tags') + + Zotero.getString('punctuation.colon'); + this.id('tagsClick').value = v; ]]></body> </method> <method name="relatedClick"> <body><![CDATA[ - Zotero.spawn(function* () { - yield this.item.loadRelations(); - var relatedList = this.item.relatedItems; - if (relatedList.length > 0) { - var x = this.boxObject.screenX; - var y = this.boxObject.screenY; - this.id('relatedPopup').openPopupAtScreen(x, y, false); - } - else { - this.id('related').add(); - } - }, this); + var relatedList = this.item.relatedItems; + if (relatedList.length > 0) { + var x = this.boxObject.screenX; + var y = this.boxObject.screenY; + this.id('relatedPopup').openPopupAtScreen(x, y, false); + } + else { + this.id('related').add(); + } ]]></body> </method> <method name="updateRelatedSummary"> <body><![CDATA[ - Zotero.spawn(function* () { - var v = yield this.id('related').summary; - - if (!v || v == "") { - v = "[" + Zotero.getString('pane.item.noteEditor.clickHere') + "]"; - } - - this.id('relatedLabel').value = Zotero.getString('itemFields.related') - + Zotero.getString('punctuation.colon'); - this.id('relatedClick').value = v; - }, this) + var v = this.id('related').summary; + + if (!v || v == "") { + v = "[" + Zotero.getString('pane.item.noteEditor.clickHere') + "]"; + } + + this.id('relatedLabel').value = Zotero.getString('itemFields.related') + + Zotero.getString('punctuation.colon'); + this.id('relatedClick').value = v; ]]></body> </method> <method name="parentClick"> diff --git a/chrome/content/zotero/bindings/relatedbox.xml b/chrome/content/zotero/bindings/relatedbox.xml @@ -74,25 +74,20 @@ <property name="summary"> <getter> <![CDATA[ - return Zotero.spawn(function* () { - var r = ""; - - if (this.item) { - yield this.item.loadRelations() - .tap(() => Zotero.Promise.check(this.item)); - var keys = this.item.relatedItems; - if (keys.length) { - let items = yield Zotero.Items.getAsync(keys) - .tap(() => Zotero.Promise.check(this.item)); - for (let item of items) { - r = r + item.getDisplayTitle() + ", "; - } - r = r.substr(0,r.length-2); + var r = ""; + + if (this.item) { + var keys = this.item.relatedItems; + if (keys.length) { + for (let key of keys) { + let item = Zotero.Items.getByLibraryAndKey(this.item.libraryID, key); + r = r + item.getDisplayTitle() + ", "; } + r = r.substr(0,r.length-2); } - - return r; - }, this); + } + + return r; ]]> </getter> </property> @@ -129,89 +124,79 @@ </method> <method name="refresh"> - <body> - <![CDATA[ - return Zotero.spawn(function* () { - var addButton = this.id('addButton'); - addButton.hidden = !this.editable; - - var rows = this.id('relatedRows'); - while(rows.hasChildNodes()) - rows.removeChild(rows.firstChild); - - if (this.item) { - yield this.item.loadRelations() - .tap(() => Zotero.Promise.check(this.item)); - var relatedKeys = this.item.relatedItems; - for (var i = 0; i < relatedKeys.length; i++) { - let key = relatedKeys[i]; - let relatedItem = - yield Zotero.Items.getByLibraryAndKeyAsync( - this.item.libraryID, key - ) - .tap(() => Zotero.Promise.check(this.item)); - let id = relatedItem.id; - yield relatedItem.loadItemData() - .tap(() => Zotero.Promise.check(this.item)); - let icon = document.createElement("image"); - icon.className = "zotero-box-icon"; - let type = Zotero.ItemTypes.getName(relatedItem.itemTypeID); - if (type=='attachment') - { - switch (relatedItem.attaachmentLinkMode) { - case Zotero.Attachments.LINK_MODE_LINKED_URL: - type += '-web-link'; - break; - - case Zotero.Attachments.LINK_MODE_IMPORTED_URL: - type += '-snapshot'; - break; - - case Zotero.Attachments.LINK_MODE_LINKED_FILE: - type += '-link'; - break; - - case Zotero.Attachments.LINK_MODE_IMPORTED_FILE: - type += '-file'; - break; - } - } - icon.setAttribute('src','chrome://zotero/skin/treeitem-' + type + '.png'); - - var label = document.createElement("label"); - label.className = "zotero-box-label"; - label.setAttribute('value', relatedItem.getDisplayTitle()); - label.setAttribute('crop','end'); - label.setAttribute('flex','1'); - - var box = document.createElement('box'); - box.setAttribute('onclick', - "document.getBindingParent(this).showItem('" + id + "')"); - box.setAttribute('class','zotero-clicky'); - box.setAttribute('flex','1'); - box.appendChild(icon); - box.appendChild(label); - - if (this.editable) { - var remove = document.createElement("label"); - remove.setAttribute('value','-'); - remove.setAttribute('onclick', - "document.getBindingParent(this).remove('" + id + "');"); - remove.setAttribute('class','zotero-clicky zotero-clicky-minus'); - } - - var row = document.createElement("row"); - row.appendChild(box); - if (this.editable) { - row.appendChild(remove); + <body><![CDATA[ + var addButton = this.id('addButton'); + addButton.hidden = !this.editable; + + var rows = this.id('relatedRows'); + while(rows.hasChildNodes()) + rows.removeChild(rows.firstChild); + + if (this.item) { + var relatedKeys = this.item.relatedItems; + for (var i = 0; i < relatedKeys.length; i++) { + let key = relatedKeys[i]; + let relatedItem = Zotero.Items.getByLibraryAndKey( + this.item.libraryID, key + ); + let id = relatedItem.id; + let icon = document.createElement("image"); + icon.className = "zotero-box-icon"; + let type = Zotero.ItemTypes.getName(relatedItem.itemTypeID); + if (type=='attachment') + { + switch (relatedItem.attaachmentLinkMode) { + case Zotero.Attachments.LINK_MODE_LINKED_URL: + type += '-web-link'; + break; + + case Zotero.Attachments.LINK_MODE_IMPORTED_URL: + type += '-snapshot'; + break; + + case Zotero.Attachments.LINK_MODE_LINKED_FILE: + type += '-link'; + break; + + case Zotero.Attachments.LINK_MODE_IMPORTED_FILE: + type += '-file'; + break; } - rows.appendChild(row); } - this.updateCount(relatedKeys.length); + icon.setAttribute('src','chrome://zotero/skin/treeitem-' + type + '.png'); + + var label = document.createElement("label"); + label.className = "zotero-box-label"; + label.setAttribute('value', relatedItem.getDisplayTitle()); + label.setAttribute('crop','end'); + label.setAttribute('flex','1'); + + var box = document.createElement('box'); + box.setAttribute('onclick', + "document.getBindingParent(this).showItem('" + id + "')"); + box.setAttribute('class','zotero-clicky'); + box.setAttribute('flex','1'); + box.appendChild(icon); + box.appendChild(label); + + if (this.editable) { + var remove = document.createElement("label"); + remove.setAttribute('value','-'); + remove.setAttribute('onclick', + "document.getBindingParent(this).remove('" + id + "');"); + remove.setAttribute('class','zotero-clicky zotero-clicky-minus'); + } + + var row = document.createElement("row"); + row.appendChild(box); + if (this.editable) { + row.appendChild(remove); + } + rows.appendChild(row); } - }, this); - ]]> - </body> + this.updateCount(relatedKeys.length); + } + ]]></body> </method> <method name="add"> <body><![CDATA[ @@ -238,13 +223,11 @@ } yield Zotero.DB.executeTransaction(function* () { for (let relItem of relItems) { - yield this.item.loadRelations(); if (this.item.addRelatedItem(relItem)) { yield this.item.save({ skipDateModifiedUpdate: true }); } - yield relItem.loadRelations(); if (relItem.addRelatedItem(this.item)) { yield relItem.save({ skipDateModifiedUpdate: true @@ -267,7 +250,6 @@ skipDateModifiedUpdate: true }); } - yield item.loadRelations(); if (item.removeRelatedItem(this.item)) { yield item.save({ skipDateModifiedUpdate: true diff --git a/chrome/content/zotero/bindings/tagsbox.xml b/chrome/content/zotero/bindings/tagsbox.xml @@ -91,24 +91,20 @@ <property name="summary"> <getter><![CDATA[ - return Zotero.spawn(function* () { - var r = ""; - - if (this.item) { - yield this.item.loadTags() - .tap(() => Zotero.Promise.check(this.mode)); - var tags = this.item.getTags(); - if (tags) { - for(var i = 0; i < tags.length; i++) - { - r = r + tags[i].tag + ", "; - } - r = r.substr(0,r.length-2); + var r = ""; + + if (this.item) { + var tags = this.item.getTags(); + if (tags) { + for(var i = 0; i < tags.length; i++) + { + r = r + tags[i].tag + ", "; } + r = r.substr(0,r.length-2); } - - return r; - }, this); + } + + return r; ]]></getter> </property> @@ -141,7 +137,8 @@ return Zotero.spawn(function* () { if (type == 'setting') { if (ids.some(function (val) val.split("/")[1] == 'tagColors') && this.item) { - return this.reload(); + this.reload(); + return; } } else if (type == 'item-tag') { @@ -198,7 +195,8 @@ } else if (type == 'tag') { if (event == 'modify') { - return this.reload(); + this.reload(); + return; } } }.bind(this)); @@ -208,41 +206,32 @@ <method name="reload"> <body><![CDATA[ - return Zotero.spawn(function* () { - Zotero.debug('Reloading tags box'); - - yield this.item.loadTags() - .tap(() => Zotero.Promise.check(this.mode)); - - // Cancel field focusing while we're updating - this._reloading = true; - - this.id('addButton').hidden = !this.editable; - - this._tagColors = yield Zotero.Tags.getColors(this.item.libraryID) - .tap(() => Zotero.Promise.check(this.mode)); - - var rows = this.id('tagRows'); - while(rows.hasChildNodes()) { - rows.removeChild(rows.firstChild); - } - var tags = this.item.getTags(); - - // Sort tags alphabetically - var collation = Zotero.getLocaleCollation(); - tags.sort(function (a, b) collation.compareString(1, a.tag, b.tag)); - - for (let i=0; i<tags.length; i++) { - this.addDynamicRow(tags[i], i+1); - } - this.updateCount(tags.length); - - this._reloading = false; - this._focusField(); - - var event = new Event('refresh'); - this.dispatchEvent(event); - }, this); + Zotero.debug('Reloading tags box'); + + // Cancel field focusing while we're updating + this._reloading = true; + + this.id('addButton').hidden = !this.editable; + + this._tagColors = Zotero.Tags.getColors(this.item.libraryID); + + var rows = this.id('tagRows'); + while(rows.hasChildNodes()) { + rows.removeChild(rows.firstChild); + } + var tags = this.item.getTags(); + + // Sort tags alphabetically + var collation = Zotero.getLocaleCollation(); + tags.sort(function (a, b) collation.compareString(1, a.tag, b.tag)); + + for (let i=0; i<tags.length; i++) { + this.addDynamicRow(tags[i], i+1); + } + this.updateCount(tags.length); + + this._reloading = false; + this._focusField(); ]]></body> </method> @@ -718,7 +707,7 @@ this._lastTabIndex = this.item.getTags().length; } - yield this.reload(); + this.reload(); } // Single tag at end else { diff --git a/chrome/content/zotero/bindings/tagselector.xml b/chrome/content/zotero/bindings/tagselector.xml @@ -236,8 +236,7 @@ var emptyRegular = true; var tagsBox = this.id('tags-box'); - var tagColors = yield Zotero.Tags.getColors(this.libraryID) - .tap(() => Zotero.Promise.check(this.mode)); + var tagColors = Zotero.Tags.getColors(this.libraryID); // If new data, rebuild boxes if (fetch || this._dirty) { @@ -245,6 +244,13 @@ .tap(() => Zotero.Promise.check(this.mode)); tagsBox.textContent = ""; + // Add colored tags that aren't already real tags + let regularTags = new Set(this._tags.map(tag => tag.tag)); + let coloredTags = new Set(tagColors.keys()); + [for (x of coloredTags) if (!regularTags.has(x)) x].forEach(x => + this._tags.push(Zotero.Tags.cleanData({ tag: x })) + ); + // Sort by name let collation = Zotero.getLocaleCollation(); this._tags.sort(function (a, b) { @@ -375,45 +381,43 @@ <method name="insertSorted"> <parameter name="tagObjs"/> <body><![CDATA[ - return Zotero.spawn(function* () { - var tagColors = yield Zotero.Tags.getColors(this._libraryID); - - var collation = Zotero.getLocaleCollation(); - tagObjs.sort(function (a, b) { - return collation.compareString(1, a.tag, b.tag); - }); - - // Create tag elements in sorted order - var tagsBox = this.id('tags-box'); - var tagElems = tagsBox.childNodes; - var j = 0; - loop: - for (let i = 0; i < tagObjs.length; i++) { - let tagObj = tagObjs[i]; - while (j < tagElems.length) { - let elem = tagElems[j]; - let comp = collation.compareString( - 1, tagObj.tag, elem.textContent - ); - // If tag already exists, update type if new one is lower - if (comp == 0) { - let tagType = elem.getAttribute('tagType'); - if (parseInt(tagObj.type) < parseInt(tagType)) { - elem.setAttribute('tagType', tagObj.type); - } - continue loop; - } - if (comp < 0) { - break; + var tagColors = Zotero.Tags.getColors(this._libraryID); + + var collation = Zotero.getLocaleCollation(); + tagObjs.sort(function (a, b) { + return collation.compareString(1, a.tag, b.tag); + }); + + // Create tag elements in sorted order + var tagsBox = this.id('tags-box'); + var tagElems = tagsBox.childNodes; + var j = 0; + loop: + for (let i = 0; i < tagObjs.length; i++) { + let tagObj = tagObjs[i]; + while (j < tagElems.length) { + let elem = tagElems[j]; + let comp = collation.compareString( + 1, tagObj.tag, elem.textContent + ); + // If tag already exists, update type if new one is lower + if (comp == 0) { + let tagType = elem.getAttribute('tagType'); + if (parseInt(tagObj.type) < parseInt(tagType)) { + elem.setAttribute('tagType', tagObj.type); } - j++; + continue loop; } - this._insertClickableTag(tagsBox, tagObj, tagElems[j]); - this._updateClickableTag( - tagElems[j], tagElems[j].textContent, tagColors - ); + if (comp < 0) { + break; + } + j++; } - }, this); + this._insertClickableTag(tagsBox, tagObj, tagElems[j]); + this._updateClickableTag( + tagElems[j], tagElems[j].textContent, tagColors + ); + } ]]></body> </method> @@ -512,7 +516,7 @@ }.bind(this)); if (tagObjs.length) { - yield this.insertSorted(tagObjs); + this.insertSorted(tagObjs); } } // Don't add anything for item or collection-item; just update scope @@ -671,7 +675,7 @@ // Colored tags don't need to exist, so in that case // just rename the color setting else { - let color = yield Zotero.Tags.getColor(this.libraryID, oldName); + let color = Zotero.Tags.getColor(this.libraryID, oldName); if (!color) { throw new Error("Can't rename missing tag"); } @@ -715,18 +719,6 @@ ]]></body> </method> - <method name="getColor"> - <parameter name="tagIDs"/> - <body><![CDATA[ - return Zotero.spawn(function* () { - tagIDs = tagIDs.split('-'); - var name = yield Zotero.Tags.getName(tagIDs[0]); - var colorData = yield Zotero.Tags.getColor(this.libraryID, name); - return colorData ? colorData.color : '#000000'; - }.bind(this)); - ]]></body> - </method> - <method name="_insertClickableTag"> <parameter name="tagsBox"/> @@ -877,7 +869,7 @@ name: name }; - var tagColors = yield Zotero.Tags.getColors(this.libraryID); + var tagColors = Zotero.Tags.getColors(this.libraryID); if (tagColors.size >= Zotero.Tags.MAX_COLORED_TAGS && !tagColors.has(io.name)) { var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"] .getService(Components.interfaces.nsIPromptService); diff --git a/chrome/content/zotero/duplicatesMerge.js b/chrome/content/zotero/duplicatesMerge.js @@ -23,10 +23,13 @@ ***** END LICENSE BLOCK ***** */ +"use strict"; + var Zotero_Duplicates_Pane = new function () { - _items = []; - _otherItems = []; - _ignoreFields = ['dateAdded', 'dateModified', 'accessDate']; + var _masterItem; + var _items = []; + var _otherItems = []; + var _ignoreFields = ['dateAdded', 'dateModified', 'accessDate']; this.setItems = function (items, displayNumItemsOnTypeError) { var itemTypeID, oldestItem, otherItems = []; @@ -77,15 +80,13 @@ var Zotero_Duplicates_Pane = new function () { // Update the UI // - var diff = oldestItem.multiDiff(otherItems, _ignoreFields); - var button = document.getElementById('zotero-duplicates-merge-button'); var versionSelect = document.getElementById('zotero-duplicates-merge-version-select'); var itembox = document.getElementById('zotero-duplicates-merge-item-box'); var fieldSelect = document.getElementById('zotero-duplicates-merge-field-select'); - versionSelect.hidden = !diff; - if (diff) { + var alternatives = oldestItem.multiDiff(otherItems, _ignoreFields); + if (alternatives) { // Populate menulist with Date Added values from all items var dateList = document.getElementById('zotero-duplicates-merge-original-date'); @@ -111,8 +112,8 @@ var Zotero_Duplicates_Pane = new function () { } button.label = Zotero.getString('pane.item.duplicates.mergeItems', (otherItems.length + 1)); - itembox.hiddenFields = diff ? [] : ['dateAdded', 'dateModified']; - fieldSelect.hidden = !diff; + versionSelect.hidden = fieldSelect.hidden = !alternatives; + itembox.hiddenFields = alternatives ? [] : ['dateAdded', 'dateModified']; this.setMaster(0); @@ -130,27 +131,25 @@ var Zotero_Duplicates_Pane = new function () { // Add master item's values to the beginning of each set of // alternative values so that they're still available if the item box // modifies the item - Zotero.spawn(function* () { - var diff = yield item.multiDiff(_otherItems, _ignoreFields); - if (diff) { - let itemValues = yield item.toJSON(); - for (let i in diff) { - diff[i].unshift(itemValues[i] !== undefined ? itemValues[i] : ''); - } - itembox.fieldAlternatives = diff; + var alternatives = item.multiDiff(_otherItems, _ignoreFields); + if (alternatives) { + let itemValues = item.toJSON(); + for (let i in alternatives) { + alternatives[i].unshift(itemValues[i] !== undefined ? itemValues[i] : ''); } - - var newItem = yield item.copy(); - yield newItem.loadItemData(); - yield newItem.loadCreators(); - itembox.item = newItem; - }); + itembox.fieldAlternatives = alternatives; + } + + _masterItem = item; + itembox.item = item.clone(); } - this.merge = function () { + this.merge = Zotero.Promise.coroutine(function* () { var itembox = document.getElementById('zotero-duplicates-merge-item-box'); Zotero.CollectionTreeCache.clear(); - Zotero.Items.merge(itembox.item, _otherItems); - } + // Update master item with any field alternatives from the item box + _masterItem.fromJSON(itembox.item.toJSON()); + Zotero.Items.merge(_masterItem, _otherItems); + }); } diff --git a/chrome/content/zotero/fileInterface.js b/chrome/content/zotero/fileInterface.js @@ -126,6 +126,7 @@ var Zotero_File_Interface = new function() { this.exportItems = exportItems; this.bibliographyFromCollection = bibliographyFromCollection; this.bibliographyFromItems = bibliographyFromItems; + this.copyItemsToClipboard = copyItemsToClipboard; this.copyCitationToClipboard = copyCitationToClipboard; /** @@ -407,7 +408,7 @@ var Zotero_File_Interface = new function() { * * Does not check that items are actual references (and not notes or attachments) */ - this.copyItemsToClipboard = Zotero.Promise.coroutine(function* (items, style, locale, asHTML, asCitations) { + function copyItemsToClipboard(items, style, locale, asHTML, asCitations) { // copy to clipboard var transferable = Components.classes["@mozilla.org/widget/transferable;1"]. createInstance(Components.interfaces.nsITransferable); @@ -417,7 +418,7 @@ var Zotero_File_Interface = new function() { var cslEngine = style.getCiteProc(locale); // add HTML - var bibliography = yield Zotero.Cite.makeFormattedBibliographyOrCitationList(cslEngine, items, "html", asCitations); + var bibliography = Zotero.Cite.makeFormattedBibliographyOrCitationList(cslEngine, items, "html", asCitations); var str = Components.classes["@mozilla.org/supports-string;1"]. createInstance(Components.interfaces.nsISupportsString); str.data = bibliography; @@ -427,7 +428,7 @@ var Zotero_File_Interface = new function() { // add text (or HTML source) if(!asHTML) { cslEngine = style.getCiteProc(locale); - var bibliography = yield Zotero.Cite.makeFormattedBibliographyOrCitationList(cslEngine, items, "text", asCitations); + var bibliography = Zotero.Cite.makeFormattedBibliographyOrCitationList(cslEngine, items, "text", asCitations); } var str = Components.classes["@mozilla.org/supports-string;1"]. createInstance(Components.interfaces.nsISupportsString); @@ -436,7 +437,7 @@ var Zotero_File_Interface = new function() { transferable.setTransferData("text/unicode", str, bibliography.length*2); clipboardService.setData(transferable, null, Components.interfaces.nsIClipboard.kGlobalClipboard); - }); + } /* @@ -483,7 +484,7 @@ var Zotero_File_Interface = new function() { /* * Shows bibliography options and creates a bibliography */ - let _doBibliographyOptions = Zotero.Promise.coroutine(function* (name, items) { + function _doBibliographyOptions(name, items) { // make sure at least one item is not a standalone note or attachment var haveRegularItem = false; for each(var item in items) { @@ -515,12 +516,12 @@ var Zotero_File_Interface = new function() { // generate bibliography try { if(io.method == 'copy-to-clipboard') { - yield Zotero_File_Interface.copyItemsToClipboard(items, io.style, locale, false, io.mode === "citations"); + Zotero_File_Interface.copyItemsToClipboard(items, io.style, locale, false, io.mode === "citations"); } else { var style = Zotero.Styles.get(io.style); var cslEngine = style.getCiteProc(locale); - var bibliography = yield Zotero.Cite.makeFormattedBibliographyOrCitationList(cslEngine, + var bibliography = Zotero.Cite.makeFormattedBibliographyOrCitationList(cslEngine, items, format, io.mode === "citations"); } } catch(e) { @@ -598,7 +599,7 @@ var Zotero_File_Interface = new function() { fStream.close(); } } - }); + } function _saveBibliography(name, format) { diff --git a/chrome/content/zotero/itemPane.js b/chrome/content/zotero/itemPane.js @@ -94,13 +94,11 @@ var ZoteroItemPane = new function() { _notesList.removeChild(_notesList.firstChild); } - yield item.loadChildItems(); let notes = yield Zotero.Items.getAsync(item.getNotes()); if (notes.length) { for (var i = 0; i < notes.length; i++) { let note = notes[i]; let id = notes[i].id; - yield note.loadItemData(); var icon = document.createElement('image'); icon.className = "zotero-box-icon"; @@ -148,7 +146,6 @@ var ZoteroItemPane = new function() { box.mode = 'edit'; } - yield Zotero.Promise.all([item.loadItemData(), item.loadCreators()]); box.item = item; }); diff --git a/chrome/content/zotero/locateMenu.js b/chrome/content/zotero/locateMenu.js @@ -400,7 +400,6 @@ var Zotero_LocateMenu = new function() { } if(item.isRegularItem()) { - yield item.loadChildItems(); var attachments = item.getAttachments(); if(attachments) { // look through url fields for non-file:/// attachments diff --git a/chrome/content/zotero/recognizePDF.js b/chrome/content/zotero/recognizePDF.js @@ -395,7 +395,6 @@ var Zotero_RecognizePDF = new function() { } // put new item in same collections as the old one - yield item.loadCollections(); let itemCollections = item.getCollections(); for (let i = 0; i < itemCollections.length; i++) { let collection = yield Zotero.Collections.getAsync(itemCollections[i]); diff --git a/chrome/content/zotero/xpcom/api.js b/chrome/content/zotero/xpcom/api.js @@ -56,7 +56,6 @@ Zotero.API = { if (!col) { throw new Error('Invalid collection ID or key'); } - yield col.loadChildItems(); results = col.getChildItems(); break; diff --git a/chrome/content/zotero/xpcom/attachments.js b/chrome/content/zotero/xpcom/attachments.js @@ -1090,8 +1090,7 @@ Zotero.Attachments = new function(){ Zotero.DB.requireTransaction(); - attachment.loadItemData(); - var newAttachment = yield attachment.clone(libraryID); + var newAttachment = attachment.clone(libraryID); if (attachment.isImportedAttachment()) { // Attachment path isn't copied over by clone() if libraryID is different newAttachment.attachmentPath = attachment.attachmentPath; diff --git a/chrome/content/zotero/xpcom/cite.js b/chrome/content/zotero/xpcom/cite.js @@ -71,9 +71,9 @@ Zotero.Cite = { * @param {String} format The format of the output (html, text, or rtf) * @return {String} Bibliography or item list in specified format */ - "makeFormattedBibliographyOrCitationList":Zotero.Promise.coroutine(function* (cslEngine, items, format, asCitationList) { + "makeFormattedBibliographyOrCitationList":function(cslEngine, items, format, asCitationList) { cslEngine.setOutputFormat(format); - yield cslEngine.updateItems(items.map(item => item.id)); + cslEngine.updateItems(items.map(item => item.id)); if(!asCitationList) { var bibliography = Zotero.Cite.makeFormattedBibliography(cslEngine, format); @@ -84,7 +84,7 @@ Zotero.Cite = { var citations=[]; for (var i=0, ilen=items.length; i<ilen; i++) { var item = items[i]; - var outList = yield cslEngine.appendCitationCluster({"citationItems":[{"id":item.id}], "properties":{}}, true); + var outList = cslEngine.appendCitationCluster({"citationItems":[{"id":item.id}], "properties":{}}, true); for (var j=0, jlen=outList.length; j<jlen; j++) { var citationPos = outList[j][0]; citations[citationPos] = outList[j][1]; @@ -124,7 +124,7 @@ Zotero.Cite = { return "<\\rtf \n"+citations.join("\\\n")+"\n}"; } } - }), + }, /** * Makes a formatted bibliography @@ -492,54 +492,21 @@ Zotero.Cite.System = function(automaticJournalAbbreviations) { if(automaticJournalAbbreviations) { this.getAbbreviation = Zotero.Cite.getAbbreviation; } - this.items = {}; } Zotero.Cite.System.prototype = { /** - * Asynchronously fetch item and convert to CSL JSON - */ - "addItem":Zotero.Promise.coroutine(function* (zoteroItem) { - if (typeof(zoteroItem) != "object") { - if (this.items.hasOwnProperty(zoteroItem)) return; - zoteroItem = yield Zotero.Items.getAsync(zoteroItem); - } - if (this.items.hasOwnProperty(zoteroItem.id)) return; - let item = yield Zotero.Utilities.itemToCSLJSON(zoteroItem); - item.id = zoteroItem.id; - - if (!Zotero.Prefs.get("export.citePaperJournalArticleURL")) { - var itemType = Zotero.ItemTypes.getName(zoteroItem.itemTypeID); - // don't return URL or accessed information for journal articles if a - // pages field exists - if (["journalArticle", "newspaperArticle", "magazineArticle"].indexOf(itemType) !== -1 - && item.pages - ) { - delete item.URL; - delete item.accessed; - } - } - this.items[item.id] = item; - }), - - /** - * Asynchronously fetch items and convert them to CSL JSON - */ - "addItems":Zotero.Promise.coroutine(function* (items) { - for (let item of items) { - yield this.addItem(item); - } - }), - - /** * citeproc-js system function for getting items * See http://gsl-nagoya-u.net/http/pub/citeproc-doc.html#retrieveitem * @param {String|Integer} Item ID, or string item for embedded citations * @return {Object} citeproc-js item */ "retrieveItem":function retrieveItem(item) { - let slashIndex; - if(typeof item === "string" && (slashIndex = item.indexOf("/")) !== -1) { + var zoteroItem, slashIndex; + if(typeof item === "object" && item !== null && item instanceof Zotero.Item) { + //if(this._cache[item.id]) return this._cache[item.id]; + zoteroItem = item; + } else if(typeof item === "string" && (slashIndex = item.indexOf("/")) !== -1) { // is an embedded item var sessionID = item.substr(0, slashIndex); var session = Zotero.Integration.sessions[sessionID] @@ -550,8 +517,36 @@ Zotero.Cite.System.prototype = { return embeddedCitation; } } + } else { + // is an item ID + //if(this._cache[item]) return this._cache[item]; + try { + zoteroItem = Zotero.Items.get(item); + } catch(e) {} } - return this.items[item]; + + if(!zoteroItem) { + throw "Zotero.Cite.System.retrieveItem called on non-item "+item; + } + + var cslItem = Zotero.Utilities.itemToCSLJSON(zoteroItem); + + // TEMP: citeproc-js currently expects the id property to be the item DB id + cslItem.id = zoteroItem.id; + + if (!Zotero.Prefs.get("export.citePaperJournalArticleURL")) { + var itemType = Zotero.ItemTypes.getName(zoteroItem.itemTypeID); + // don't return URL or accessed information for journal articles if a + // pages field exists + if (["journalArticle", "newspaperArticle", "magazineArticle"].indexOf(itemType) !== -1 + && zoteroItem.getField("pages") + ) { + delete cslItem.URL; + delete cslItem.accessed; + } + } + + return cslItem; }, /** @@ -579,20 +574,3 @@ Zotero.Cite.System.prototype = { return str.value; } }; - -Zotero.Cite.AsyncCiteProc = function() { - Zotero.CiteProc.CSL.Engine.apply(this, arguments); -} -Zotero.Cite.AsyncCiteProc.prototype = Object.create(Zotero.CiteProc.CSL.Engine.prototype); -Zotero.Cite.AsyncCiteProc.prototype.updateItems = Zotero.Promise.coroutine(function*(items) { - yield this.sys.addItems(items); - Zotero.CiteProc.CSL.Engine.prototype.updateItems.call(this, items); -}); -Zotero.Cite.AsyncCiteProc.prototype.appendCitationCluster = Zotero.Promise.coroutine(function*(citation, isRegistered) { - if (!isRegistered) { - for (let citationItem of citation.citationItems) { - yield this.sys.addItem(citationItem.id); - } - } - return Zotero.CiteProc.CSL.Engine.prototype.appendCitationCluster.call(this, citation, isRegistered); -}); diff --git a/chrome/content/zotero/xpcom/collectionTreeRow.js b/chrome/content/zotero/xpcom/collectionTreeRow.js @@ -295,6 +295,7 @@ Zotero.CollectionTreeRow.prototype.getSearchObject = Zotero.Promise.coroutine(fu // Create the outer (filter) search var s2 = new Zotero.Search(); + if (this.isTrash()) { s2.addCondition('deleted', 'true'); } diff --git a/chrome/content/zotero/xpcom/collectionTreeView.js b/chrome/content/zotero/xpcom/collectionTreeView.js @@ -67,6 +67,9 @@ Zotero.CollectionTreeView.prototype.type = 'collection'; Object.defineProperty(Zotero.CollectionTreeView.prototype, "selectedTreeRow", { get: function () { + if (!this.selection || !this.selection.count) { + return false; + } return this.getRow(this.selection.currentIndex); } }); @@ -156,25 +159,28 @@ Zotero.CollectionTreeView.prototype.refresh = Zotero.Promise.coroutine(function* this._containerState = {}; } - if (this.hideSources.indexOf('duplicates') == -1) { - try { - this._duplicateLibraries = Zotero.Prefs.get('duplicateLibraries').split(',').map(function (val) parseInt(val)); + var userLibraryID = Zotero.Libraries.userLibraryID; + + var readPref = function (pref) { + let ids = Zotero.Prefs.get(pref); + if (ids === "") { + this["_" + pref] = []; } - catch (e) { - // Add to personal library by default - Zotero.Prefs.set('duplicateLibraries', '0'); - this._duplicateLibraries = [0]; + else { + if (ids === undefined || typeof ids != 'string') { + ids = "" + userLibraryID; + Zotero.Prefs.set(pref, "" + userLibraryID); + } + this["_" + pref] = ids.split(',') + // Convert old id and convert to int + .map(id => id === "0" ? userLibraryID : parseInt(id)); } - } + }.bind(this); - try { - this._unfiledLibraries = Zotero.Prefs.get('unfiledLibraries').split(',').map(function (val) parseInt(val)); - } - catch (e) { - // Add to personal library by default - Zotero.Prefs.set('unfiledLibraries', '0'); - this._unfiledLibraries = [0]; + if (this.hideSources.indexOf('duplicates') == -1) { + readPref('duplicateLibraries'); } + readPref('unfiledLibraries'); var oldCount = this.rowCount || 0; var newRows = []; @@ -526,7 +532,7 @@ Zotero.CollectionTreeView.prototype._addSortedRow = Zotero.Promise.coroutine(fun ); } else if (objectType == 'search') { - let search = yield Zotero.Searches.getAsync(id); + let search = Zotero.Searches.get(id); let libraryID = search.libraryID; let startRow = this._rowMap['L' + libraryID]; @@ -545,7 +551,6 @@ Zotero.CollectionTreeView.prototype._addSortedRow = Zotero.Promise.coroutine(fun var inSearches = false; for (let i = startRow; i < this.rowCount; i++) { let treeRow = this.getRow(i); - Zotero.debug(treeRow.id); beforeRow = i; // If we've reached something other than collections, stop @@ -900,8 +905,7 @@ Zotero.CollectionTreeView.prototype.selectByID = Zotero.Promise.coroutine(functi switch (type) { case 'L': - var found = yield this.selectLibrary(id); - break; + return yield this.selectLibrary(id); case 'C': var found = yield this.expandToCollection(id); @@ -913,14 +917,13 @@ Zotero.CollectionTreeView.prototype.selectByID = Zotero.Promise.coroutine(functi break; case 'T': - var found = yield this.selectTrash(id); - break; + return yield this.selectTrash(id); } - if (!found) { + var row = this._rowMap[type + id]; + if (!row) { return false; } - var row = this._rowMap[type + id]; this._treebox.ensureRowIsVisible(row); yield this.selectWait(row); @@ -940,7 +943,7 @@ Zotero.CollectionTreeView.prototype.selectLibrary = Zotero.Promise.coroutine(fun } // Check if library is already selected - if (this.selection.currentIndex != -1) { + if (this.selection && this.selection.count && this.selection.currentIndex != -1) { var treeRow = this.getRow(this.selection.currentIndex); if (treeRow.isLibrary(true) && treeRow.ref.libraryID == libraryID) { this._treebox.ensureRowIsVisible(this.selection.currentIndex); @@ -972,7 +975,7 @@ Zotero.CollectionTreeView.prototype.selectSearch = function (id) { Zotero.CollectionTreeView.prototype.selectTrash = Zotero.Promise.coroutine(function* (libraryID) { // Check if trash is already selected - if (this.selection.currentIndex != -1) { + if (this.selection && this.selection.count && this.selection.currentIndex != -1) { let itemGroup = this.getRow(this.selection.currentIndex); if (itemGroup.isTrash() && itemGroup.ref.libraryID == libraryID) { this._treebox.ensureRowIsVisible(this.selection.currentIndex); @@ -1196,6 +1199,8 @@ Zotero.CollectionTreeView.prototype._expandRow = Zotero.Promise.coroutine(functi * Returns libraryID or FALSE if not a library */ Zotero.CollectionTreeView.prototype.getSelectedLibraryID = function() { + if (!this.selection || !this.selection.count || this.selection.currentIndex == -1) return false; + var treeRow = this.getRow(this.selection.currentIndex); return treeRow && treeRow.ref && treeRow.ref.libraryID !== undefined && treeRow.ref.libraryID; @@ -1504,10 +1509,6 @@ Zotero.CollectionTreeView.prototype.canDropCheckAsync = Zotero.Promise.coroutine } if (dataType == 'zotero/item') { - if (treeRow.isCollection()) { - yield treeRow.ref.loadChildItems(); - } - var ids = data; var items = Zotero.Items.get(ids); var skip = true; @@ -1627,7 +1628,6 @@ Zotero.CollectionTreeView.prototype.drop = Zotero.Promise.coroutine(function* (r // If linked item is in the trash, undelete it and remove it from collections // (since it shouldn't be restored to previous collections) if (linkedItem.deleted) { - yield linkedItem.loadCollections(); linkedItem.setCollections(); linkedItem.deleted = false; yield linkedItem.save({ @@ -1693,7 +1693,7 @@ Zotero.CollectionTreeView.prototype.drop = Zotero.Promise.coroutine(function* (r } // Create new clone item in target library - var newItem = yield item.clone(targetLibraryID, false, !options.tags); + var newItem = item.clone(targetLibraryID, false, !options.tags); // Set Rights field for My Publications if (options.license) { @@ -1717,11 +1717,10 @@ Zotero.CollectionTreeView.prototype.drop = Zotero.Promise.coroutine(function* (r // Child notes if (options.childNotes) { - yield item.loadChildItems(); var noteIDs = item.getNotes(); - var notes = yield Zotero.Items.getAsync(noteIDs); + var notes = Zotero.Items.get(noteIDs); for each(var note in notes) { - let newNote = yield note.clone(targetLibraryID); + let newNote = note.clone(targetLibraryID); newNote.parentID = newItemID; yield newNote.save({ skipSelect: true @@ -1733,9 +1732,8 @@ Zotero.CollectionTreeView.prototype.drop = Zotero.Promise.coroutine(function* (r // Child attachments if (options.childLinks || options.childFileAttachments) { - yield item.loadChildItems(); var attachmentIDs = item.getAttachments(); - var attachments = yield Zotero.Items.getAsync(attachmentIDs); + var attachments = Zotero.Items.get(attachmentIDs); for each(var attachment in attachments) { var linkMode = attachment.attachmentLinkMode; @@ -1864,8 +1862,8 @@ Zotero.CollectionTreeView.prototype.drop = Zotero.Promise.coroutine(function* (r if (targetTreeRow.isPublications()) { let items = yield Zotero.Items.getAsync(ids); - let io = yield this._treebox.treeBody.ownerDocument.defaultView.ZoteroPane - .showPublicationsWizard(items); + let io = this._treebox.treeBody.ownerDocument.defaultView + .ZoteroPane.showPublicationsWizard(items); if (!io) { return; } diff --git a/chrome/content/zotero/xpcom/data/collection.js b/chrome/content/zotero/xpcom/data/collection.js @@ -28,11 +28,8 @@ Zotero.Collection = function(params = {}) { this._name = null; - this._hasChildCollections = null; - this._childCollections = []; - - this._hasChildItems = false; - this._childItems = []; + this._childCollections = new Set(); + this._childItems = new Set(); Zotero.Utilities.assignProps(this, params, ['name', 'libraryID', 'parentID', 'parentKey', 'lastSync']); @@ -162,19 +159,13 @@ Zotero.Collection.prototype.loadFromRow = function(row) { Zotero.Collection.prototype.hasChildCollections = function() { - if (this._hasChildCollections !== null) { - return this._hasChildCollections; - } - this._requireData('primaryData'); - return false; + this._requireData('childCollections'); + return this._childCollections.size > 0; } Zotero.Collection.prototype.hasChildItems = function() { - if (this._hasChildItems !== null) { - return this._hasChildItems; - } - this._requireData('primaryData'); - return false; + this._requireData('childItems'); + return this._childItems.size > 0; } @@ -189,19 +180,11 @@ Zotero.Collection.prototype.getChildCollections = function (asIDs) { // Return collectionIDs if (asIDs) { - var ids = []; - for each(var col in this._childCollections) { - ids.push(col.id); - } - return ids; + return this._childCollections.values(); } // Return Zotero.Collection objects - var objs = []; - for each(var col in this._childCollections) { - objs.push(col); - } - return objs; + return Array.from(this._childCollections).map(id => this.ObjectsClass.get(id)); } @@ -215,13 +198,14 @@ Zotero.Collection.prototype.getChildCollections = function (asIDs) { Zotero.Collection.prototype.getChildItems = function (asIDs, includeDeleted) { this._requireData('childItems'); - if (this._childItems.length == 0) { + if (this._childItems.size == 0) { return []; } // Remove deleted items if necessary var childItems = []; - for each(var item in this._childItems) { + for (let itemID of this._childItems) { + let item = this.ChildObjects.get(itemID); if (includeDeleted || !item.deleted) { childItems.push(item); } @@ -229,19 +213,11 @@ Zotero.Collection.prototype.getChildItems = function (asIDs, includeDeleted) { // Return itemIDs if (asIDs) { - var ids = []; - for each(var item in childItems) { - ids.push(item.id); - } - return ids; + return childItems.map(item => item.id); } // Return Zotero.Item objects - var objs = []; - for each(var item in childItems) { - objs.push(item); - } - return objs; + return childItems.slice(); } Zotero.Collection.prototype._initSave = Zotero.Promise.coroutine(function* (env) { @@ -388,7 +364,6 @@ Zotero.Collection.prototype.addItems = Zotero.Promise.coroutine(function* (itemI return; } - yield this.loadChildItems(); var current = this.getChildItems(true); Zotero.DB.requireTransaction(); @@ -400,15 +375,14 @@ Zotero.Collection.prototype.addItems = Zotero.Promise.coroutine(function* (itemI continue; } - let item = yield this.ChildObjects.getAsync(itemID); - yield item.loadCollections(); + let item = this.ChildObjects.get(itemID); item.addToCollection(this.id); yield item.save({ skipDateModifiedUpdate: true }); } - yield this.loadChildItems(true); + yield this._loadDataType('childItems'); }); /** @@ -434,7 +408,6 @@ Zotero.Collection.prototype.removeItems = Zotero.Promise.coroutine(function* (it return; } - yield this.loadChildItems(); var current = this.getChildItems(true); return Zotero.DB.executeTransaction(function* () { @@ -447,7 +420,6 @@ Zotero.Collection.prototype.removeItems = Zotero.Promise.coroutine(function* (it } let item = yield this.ChildObjects.getAsync(itemID); - yield item.loadCollections(); item.removeFromCollection(this.id); yield item.save({ skipDateModifiedUpdate: true @@ -455,7 +427,7 @@ Zotero.Collection.prototype.removeItems = Zotero.Promise.coroutine(function* (it } }.bind(this)); - yield this.loadChildItems(true); + yield this._loadDataType('childItems'); }); @@ -464,13 +436,7 @@ Zotero.Collection.prototype.removeItems = Zotero.Promise.coroutine(function* (it **/ Zotero.Collection.prototype.hasItem = function(itemID) { this._requireData('childItems'); - - for (let i=0; i<this._childItems.length; i++) { - if (this._childItems[i].id == itemID) { - return true; - } - } - return false; + return this._childItems.has(itemID); } @@ -692,8 +658,8 @@ Zotero.Collection.prototype.fromJSON = function (json) { } -Zotero.Collection.prototype.toResponseJSON = Zotero.Promise.coroutine(function* (options = {}) { - var json = yield this.constructor._super.prototype.toResponseJSON.apply(this, options); +Zotero.Collection.prototype.toResponseJSON = function (options = {}) { + var json = this.constructor._super.prototype.toResponseJSON.apply(this, options); // TODO: library block? @@ -713,10 +679,10 @@ Zotero.Collection.prototype.toResponseJSON = Zotero.Promise.coroutine(function* json.meta.numChildren = this.numChildren(); } return json; -}) +}; -Zotero.Collection.prototype.toJSON = Zotero.Promise.coroutine(function* (options = {}) { +Zotero.Collection.prototype.toJSON = function (options = {}) { var env = this._preToJSON(options); var mode = env.mode; @@ -729,7 +695,7 @@ Zotero.Collection.prototype.toJSON = Zotero.Promise.coroutine(function* (options obj.relations = {}; // TEMP return this._postToJSON(env); -}); +} /** @@ -865,75 +831,6 @@ Zotero.Collection.prototype.addLinkedCollection = Zotero.Promise.coroutine(funct // // Private methods // -Zotero.Collection.prototype.reloadHasChildCollections = Zotero.Promise.coroutine(function* () { - var sql = "SELECT COUNT(*) FROM collections WHERE parentCollectionID=?"; - this._hasChildCollections = !!(yield Zotero.DB.valueQueryAsync(sql, this.id)); -}); - - -Zotero.Collection.prototype.loadChildCollections = Zotero.Promise.coroutine(function* (reload) { - if (this._loaded.childCollections && !reload) { - return; - } - - var sql = "SELECT collectionID FROM collections WHERE parentCollectionID=?"; - var ids = yield Zotero.DB.columnQueryAsync(sql, this.id); - - this._childCollections = []; - - if (ids.length) { - for each(var id in ids) { - var col = yield this.ObjectsClass.getAsync(id); - if (!col) { - throw new Error('Collection ' + id + ' not found'); - } - this._childCollections.push(col); - } - this._hasChildCollections = true; - } - else { - this._hasChildCollections = false; - } - - this._loaded.childCollections = true; - this._clearChanged('childCollections'); -}); - - -Zotero.Collection.prototype.reloadHasChildItems = Zotero.Promise.coroutine(function* () { - var sql = "SELECT COUNT(*) FROM collectionItems WHERE collectionID=?"; - this._hasChildItems = !!(yield Zotero.DB.valueQueryAsync(sql, this.id)); -}); - - -Zotero.Collection.prototype.loadChildItems = Zotero.Promise.coroutine(function* (reload) { - if (this._loaded.childItems && !reload) { - return; - } - - var sql = "SELECT itemID FROM collectionItems WHERE collectionID=? " - // DEBUG: Fix for child items created via context menu on parent within - // a collection being added to the current collection - + "AND itemID NOT IN " - + "(SELECT itemID FROM itemNotes WHERE parentItemID IS NOT NULL) " - + "AND itemID NOT IN " - + "(SELECT itemID FROM itemAttachments WHERE parentItemID IS NOT NULL)"; - var ids = yield Zotero.DB.columnQueryAsync(sql, this.id); - - this._childItems = []; - - if (ids.length) { - var items = yield this.ChildObjects.getAsync(ids); - if (items) { - this._childItems = items; - } - } - - this._loaded.childItems = true; - this._clearChanged('childItems'); -}); - - /** * Add a collection to the cached child collections list if loaded */ @@ -941,8 +838,7 @@ Zotero.Collection.prototype._registerChildCollection = function (collectionID) { if (this._loaded.childCollections) { let collection = this.ObjectsClass.get(collectionID); if (collection) { - this._hasChildCollections = true; - this._childCollections.push(collection); + this._childCollections.add(collectionID); } } } @@ -953,13 +849,7 @@ Zotero.Collection.prototype._registerChildCollection = function (collectionID) { */ Zotero.Collection.prototype._unregisterChildCollection = function (collectionID) { if (this._loaded.childCollections) { - for (let i = 0; i < this._childCollections.length; i++) { - if (this._childCollections[i].id == collectionID) { - this._childCollections.splice(i, 1); - break; - } - } - this._hasChildCollections = this._childCollections.length > 0; + this._childCollections.delete(collectionID); } } @@ -971,8 +861,7 @@ Zotero.Collection.prototype._registerChildItem = function (itemID) { if (this._loaded.childItems) { let item = this.ChildObjects.get(itemID); if (item) { - this._hasChildItems = true; - this._childItems.push(item); + this._childItems.add(itemID); } } } @@ -983,12 +872,6 @@ Zotero.Collection.prototype._registerChildItem = function (itemID) { */ Zotero.Collection.prototype._unregisterChildItem = function (itemID) { if (this._loaded.childItems) { - for (let i = 0; i < this._childItems.length; i++) { - if (this._childItems[i].id == itemID) { - this._childItems.splice(i, 1); - break; - } - } - this._hasChildItems = this._childItems.length > 0; + this._childItems.delete(itemID); } } diff --git a/chrome/content/zotero/xpcom/data/collections.js b/chrome/content/zotero/xpcom/data/collections.js @@ -85,8 +85,7 @@ Zotero.Collections = function() { let children; if (parentID) { - let parent = yield Zotero.Collections.getAsync(parentID); - yield parent.loadChildCollections(); + let parent = Zotero.Collections.get(parentID); children = parent.getChildCollections(); } else if (libraryID) { let sql = "SELECT collectionID AS id FROM collections " @@ -156,6 +155,103 @@ Zotero.Collections = function() { } + this._loadChildCollections = Zotero.Promise.coroutine(function* (libraryID, ids, idSQL) { + var sql = "SELECT C1.collectionID, C2.collectionID AS childCollectionID " + + "FROM collections C1 LEFT JOIN collections C2 ON (C1.collectionID=C2.parentCollectionID) " + + "WHERE C1.libraryID=?" + + (ids.length ? " AND C1.collectionID IN (" + ids.map(id => parseInt(id)).join(", ") + ")" : ""); + var params = [libraryID]; + var lastID; + var rows = []; + var setRows = function (collectionID, rows) { + var collection = this._objectCache[collectionID]; + if (!collection) { + throw new Error("Collection " + collectionID + " not found"); + } + + collection._childCollections = new Set(rows); + collection._loaded.childCollections = true; + collection._clearChanged('childCollections'); + }.bind(this); + + yield Zotero.DB.queryAsync( + sql, + params, + { + noCache: ids.length != 1, + onRow: function (row) { + let collectionID = row.getResultByIndex(0); + + if (lastID && collectionID !== lastID) { + setRows(lastID, rows); + rows = []; + } + + lastID = collectionID; + + let childCollectionID = row.getResultByIndex(1); + // No child collections + if (childCollectionID === null) { + return; + } + rows.push(childCollectionID); + } + } + ); + if (lastID) { + setRows(lastID, rows); + } + }); + + + this._loadChildItems = Zotero.Promise.coroutine(function* (libraryID, ids, idSQL) { + var sql = "SELECT collectionID, itemID FROM collections " + + "LEFT JOIN collectionItems USING (collectionID) " + + "WHERE libraryID=?" + idSQL; + var params = [libraryID]; + var lastID; + var rows = []; + var setRows = function (collectionID, rows) { + var collection = this._objectCache[collectionID]; + if (!collection) { + throw new Error("Collection " + collectionID + " not found"); + } + + collection._childItems = new Set(rows); + collection._loaded.childItems = true; + collection._clearChanged('childItems'); + }.bind(this); + + yield Zotero.DB.queryAsync( + sql, + params, + { + noCache: ids.length != 1, + onRow: function (row) { + let collectionID = row.getResultByIndex(0); + + if (lastID && collectionID !== lastID) { + setRows(lastID, rows); + rows = []; + } + + lastID = collectionID; + + let itemID = row.getResultByIndex(1); + // No child items + if (itemID === null) { + return; + } + rows.push(itemID); + } + } + ); + if (lastID) { + setRows(lastID, rows); + } + }); + + this.registerChildCollection = function (collectionID, childCollectionID) { if (this._objectCache[collectionID]) { this._objectCache[collectionID]._registerChildCollection(childCollectionID); diff --git a/chrome/content/zotero/xpcom/data/creators.js b/chrome/content/zotero/xpcom/data/creators.js @@ -30,29 +30,35 @@ Zotero.Creators = new function() { var _cache = {}; + this.init = Zotero.Promise.coroutine(function* () { + var sql = "SELECT * FROM creators"; + var rows = yield Zotero.DB.queryAsync(sql); + for (let i = 0; i < rows.length; i++) { + let row = rows[i]; + _cache[row.creatorID] = this.cleanData({ + // Avoid "DB column 'name' not found" warnings from the DB row Proxy + firstName: row.firstName, + lastName: row.lastName, + fieldMode: row.fieldMode + }); + } + }); + /* * Returns creator data in internal format for a given creatorID */ - this.getAsync = Zotero.Promise.coroutine(function* (creatorID) { + this.get = function (creatorID) { if (!creatorID) { throw new Error("creatorID not provided"); } - if (_cache[creatorID]) { - return this.cleanData(_cache[creatorID]); - } - - var sql = "SELECT * FROM creators WHERE creatorID=?"; - var row = yield Zotero.DB.rowQueryAsync(sql, creatorID); - if (!row) { + if (!_cache[creatorID]) { throw new Error("Creator " + creatorID + " not found"); } - return _cache[creatorID] = this.cleanData({ - firstName: row.firstName, // avoid "DB column 'name' not found" warnings from the DB row Proxy - lastName: row.lastName, - fieldMode: row.fieldMode - }); - }); + + // Return copy of data + return this.cleanData(_cache[creatorID]); + }; this.getItemsWithCreator = function (creatorID) { @@ -87,12 +93,10 @@ Zotero.Creators = new function() { id = yield Zotero.ID.get('creators'); let sql = "INSERT INTO creators (creatorID, firstName, lastName, fieldMode) " + "VALUES (?, ?, ?, ?)"; - let insertID = yield Zotero.DB.queryAsync( + yield Zotero.DB.queryAsync( sql, [id, data.firstName, data.lastName, data.fieldMode] ); - if (!id) { - id = insertID; - } + _cache[id] = data; } return id; }); diff --git a/chrome/content/zotero/xpcom/data/dataObject.js b/chrome/content/zotero/xpcom/data/dataObject.js @@ -401,7 +401,7 @@ Zotero.DataObject.prototype.setRelations = function (newRelations) { // Relations are stored internally as a flat array with individual predicate-object pairs, // so convert the incoming relations to that - var newRelationsFlat = this._flattenRelations(newRelations); + var newRelationsFlat = this.ObjectsClass.flattenRelations(newRelations); var changed = false; if (oldRelations.length != newRelationsFlat.length) { @@ -457,8 +457,6 @@ Zotero.DataObject.prototype._getLinkedObject = Zotero.Promise.coroutine(function throw new Error(this._ObjectType + " is already in library " + libraryID); } - yield this.loadRelations(); - var predicate = Zotero.Relations.linkedObjectPredicate; var libraryObjectPrefix = Zotero.URI.getLibraryURI(libraryID) + "/" + this._objectTypePlural + "/"; @@ -514,8 +512,6 @@ Zotero.DataObject.prototype._addLinkedObject = Zotero.Promise.coroutine(function throw new Error("Can't add linked " + this._objectType + " in same library"); } - yield this.loadRelations(); - var predicate = Zotero.Relations.linkedObjectPredicate; var thisURI = Zotero.URI['get' + this._ObjectType + 'URI'](this); var objectURI = Zotero.URI['get' + this._ObjectType + 'URI'](object); @@ -539,7 +535,6 @@ Zotero.DataObject.prototype._addLinkedObject = Zotero.Promise.coroutine(function }); } else { - yield object.loadRelations(); object.addRelation(predicate, thisURI); yield object.save({ skipDateModifiedUpdate: true, @@ -551,9 +546,11 @@ Zotero.DataObject.prototype._addLinkedObject = Zotero.Promise.coroutine(function }); -/* - * Build object from database - */ +// +// Bulk data loading functions +// +// These are called by Zotero.DataObjects.prototype._loadDataType(). +// Zotero.DataObject.prototype.loadPrimaryData = Zotero.Promise.coroutine(function* (reload, failOnMissing) { if (this._loaded.primaryData && !reload) return; @@ -610,65 +607,6 @@ Zotero.DataObject.prototype.loadPrimaryData = Zotero.Promise.coroutine(function* }); -Zotero.DataObject.prototype.loadRelations = Zotero.Promise.coroutine(function* (reload) { - if (!this.ObjectsClass._relationsTable) { - throw new Error("Relations not supported for " + this._objectTypePlural); - } - - if (this._loaded.relations && !reload) { - return; - } - - Zotero.debug("Loading relations for " + this._objectType + " " + this.libraryKey); - - this._requireData('primaryData'); - - var sql = "SELECT predicate, object FROM " + this.ObjectsClass._relationsTable + " " - + "JOIN relationPredicates USING (predicateID) " - + "WHERE " + this.ObjectsClass.idColumn + "=?"; - var rows = yield Zotero.DB.queryAsync(sql, this.id); - - var relations = {}; - function addRel(predicate, object) { - if (!relations[predicate]) { - relations[predicate] = []; - } - relations[predicate].push(object); - } - - for (let i = 0; i < rows.length; i++) { - let row = rows[i]; - addRel(row.predicate, row.object); - } - - /*if (this._objectType == 'item') { - let getURI = Zotero.URI["get" + this._ObjectType + "URI"].bind(Zotero.URI); - let objectURI = getURI(this); - - // Related items are bidirectional, so include any pointing to this object - let objects = yield Zotero.Relations.getByPredicateAndObject( - Zotero.Relations.relatedItemPredicate, objectURI - ); - for (let i = 0; i < objects.length; i++) { - addRel(Zotero.Relations.relatedItemPredicate, getURI(objects[i])); - } - - // Also include any owl:sameAs relations pointing to this object - objects = yield Zotero.Relations.getByPredicateAndObject( - Zotero.Relations.linkedObjectPredicate, objectURI - ); - for (let i = 0; i < objects.length; i++) { - addRel(Zotero.Relations.linkedObjectPredicate, getURI(objects[i])); - } - }*/ - - // Relations are stored as predicate-object pairs - this._relations = this._flattenRelations(relations); - this._loaded.relations = true; - this._clearChanged('relations'); -}); - - /** * Reloads loaded, changed data * @@ -735,7 +673,7 @@ Zotero.DataObject.prototype._requireData = function (dataType) { * @param {Boolean} reload */ Zotero.DataObject.prototype._loadDataType = function (dataType, reload) { - return this["load" + dataType[0].toUpperCase() + dataType.substr(1)](reload); + return this._ObjectsClass._loadDataType(dataType, this.libraryID, [this.id]); } Zotero.DataObject.prototype.loadAllData = function (reload) { @@ -868,6 +806,16 @@ Zotero.DataObject.prototype.save = Zotero.Promise.coroutine(function* (options) Zotero.debug('Updating database with new ' + this._objectType + ' data', 4); } + if (env.options.skipAll) { + [ + 'skipDateModifiedUpdate', + 'skipClientDateModifiedUpdate', + 'skipSyncedUpdate', + 'skipEditCheck', + 'skipSelect' + ].forEach(x => env.options[x] = true); + } + try { if (Zotero.DataObject.prototype._finalizeSave == this._finalizeSave) { throw new Error("_finalizeSave not implemented for Zotero." + this._ObjectType); @@ -1214,23 +1162,19 @@ Zotero.DataObject.prototype._finalizeErase = Zotero.Promise.coroutine(function* }); -Zotero.DataObject.prototype.toResponseJSON = Zotero.Promise.coroutine(function* (options) { +Zotero.DataObject.prototype.toResponseJSON = function (options) { // TODO: library block? return { key: this.key, version: this.version, meta: {}, - data: yield this.toJSON(options) + data: this.toJSON(options) }; -}); +} Zotero.DataObject.prototype._preToJSON = function (options) { - if (!this._id) { - throw new Error(`${this._ObjectType} must be saved before running toJSON()`); - } - var env = { options }; env.mode = options.mode || 'new'; if (env.mode == 'patch') { @@ -1272,32 +1216,3 @@ Zotero.DataObject.prototype._disabledCheck = function () { + "use Zotero." + this._ObjectTypePlural + ".getAsync()"); } } - - -/** - * Flatten API JSON relations object into an array of unique predicate-object pairs - * - * @param {Object} relations - Relations object in API JSON format, with predicates as keys - * and arrays of URIs as objects - * @return {Array[]} - Predicate-object pairs - */ -Zotero.DataObject.prototype._flattenRelations = function (relations) { - var relationsFlat = []; - for (let predicate in relations) { - let object = relations[predicate]; - if (Array.isArray(object)) { - object = Zotero.Utilities.arrayUnique(object); - for (let i = 0; i < object.length; i++) { - relationsFlat.push([predicate, object[i]]); - } - } - else if (typeof object == 'string') { - relationsFlat.push([predicate, object]); - } - else { - Zotero.debug(object, 1); - throw new Error("Invalid relation value"); - } - } - return relationsFlat; -} diff --git a/chrome/content/zotero/xpcom/data/dataObjects.js b/chrome/content/zotero/xpcom/data/dataObjects.js @@ -337,6 +337,254 @@ Zotero.DataObjects.prototype.getNewer = Zotero.Promise.method(function (libraryI /** + * Loads data for a given data type + * @param {String} dataType + * @param {Integer} libraryID + * @param {Integer[]} [ids] + */ +Zotero.DataObjects.prototype._loadDataType = Zotero.Promise.coroutine(function* (dataType, libraryID, ids) { + var funcName = "_load" + dataType[0].toUpperCase() + dataType.substr(1) + // Single data types need an 's' (e.g., 'note' -> 'loadNotes()') + + ((dataType.endsWith('s') || dataType.endsWith('Data') ? '' : 's')); + if (!this[funcName]) { + throw new Error(`Zotero.${this._ZDO_Objects}.${funcName} is not a function`); + } + + if (ids && ids.length == 0) { + return; + } + + var t = new Date; + var libraryName = Zotero.Libraries.get(libraryID).name; + + var idSQL = ""; + if (ids) { + idSQL = " AND " + this.idColumn + " IN (" + ids.map(id => parseInt(id)).join(", ") + ")"; + } + + Zotero.debug("Loading " + dataType + + (ids + ? " for " + ids.length + " " + (ids.length == 1 ? this._ZDO_object : this._ZDO_objects) + : '') + + " in " + libraryName); + + yield this[funcName](libraryID, ids ? ids : [], idSQL); + + Zotero.debug(`Loaded ${dataType} in ${libraryName} in ${new Date() - t} ms`); +}); + +Zotero.DataObjects.prototype.loadAll = Zotero.Promise.coroutine(function* (libraryID, ids) { + var t = new Date(); + var libraryName = Zotero.Libraries.get(libraryID).name; + + Zotero.debug("Loading " + + (ids ? ids.length : "all") + " " + + (ids && ids.length == 1 ? this._ZDO_object : this._ZDO_objects) + + " in " + libraryName); + + let dataTypes = this.ObjectClass.prototype._dataTypes; + for (let i = 0; i < dataTypes.length; i++) { + yield this._loadDataType(dataTypes[i], libraryID, ids); + } + + Zotero.debug(`Loaded all data in ${libraryName} in ${new Date() - t} ms`); +}); + + +Zotero.DataObjects.prototype._loadPrimaryData = Zotero.Promise.coroutine(function* (libraryID, ids, idSQL, options) { + var loaded = {}; + + // If library isn't an integer (presumably false or null), skip it + if (parseInt(libraryID) != libraryID) { + libraryID = false; + } + + var sql = this.primaryDataSQL; + var params = []; + if (libraryID !== false) { + sql += ' AND O.libraryID=?'; + params.push(libraryID); + } + if (ids.length) { + sql += ' AND O.' + this._ZDO_id + ' IN (' + ids.join(',') + ')'; + } + + yield Zotero.DB.queryAsync( + sql, + params, + { + onRow: function (row) { + var id = row.getResultByName(this._ZDO_id); + var columns = Object.keys(this._primaryDataSQLParts); + var rowObj = {}; + for (let i=0; i<columns.length; i++) { + rowObj[columns[i]] = row.getResultByIndex(i); + } + var obj; + + // Existing object -- reload in place + if (this._objectCache[id]) { + this._objectCache[id].loadFromRow(rowObj, true); + obj = this._objectCache[id]; + } + // Object doesn't exist -- create new object and stuff in cache + else { + obj = this._getObjectForRow(rowObj); + obj.loadFromRow(rowObj, true); + if (!options || !options.noCache) { + this.registerObject(obj); + } + } + loaded[id] = obj; + }.bind(this) + } + ); + + if (!ids) { + this._loadedLibraries[libraryID] = true; + + // If loading all objects, remove cached objects that no longer exist + for (let i in this._objectCache) { + let obj = this._objectCache[i]; + if (libraryID !== false && obj.libraryID !== libraryID) { + continue; + } + if (!loaded[obj.id]) { + this.unload(obj.id); + } + } + + if (this._postLoad) { + this._postLoad(libraryID, ids); + } + } + + return loaded; +}); + + +Zotero.DataObjects.prototype._loadRelations = Zotero.Promise.coroutine(function* (libraryID, ids, idSQL) { + if (!this._relationsTable) { + throw new Error("Relations not supported for " + this._ZDO_objects); + } + + var sql = "SELECT " + this.idColumn + ", predicate, object " + + `FROM ${this.table} LEFT JOIN ${this._relationsTable} USING (${this.idColumn}) ` + + "LEFT JOIN relationPredicates USING (predicateID) " + + "WHERE libraryID=?" + idSQL; + var params = [libraryID]; + + var lastID; + var rows = []; + var setRows = function (id, rows) { + var obj = this._objectCache[id]; + if (!obj) { + throw new Error(this._ZDO_Object + " " + id + " not found"); + } + + var relations = {}; + function addRel(predicate, object) { + if (!relations[predicate]) { + relations[predicate] = []; + } + relations[predicate].push(object); + } + + for (let i = 0; i < rows.length; i++) { + let row = rows[i]; + addRel(row.predicate, row.object); + } + + /*if (this._objectType == 'item') { + let getURI = Zotero.URI["get" + this._ObjectType + "URI"].bind(Zotero.URI); + let objectURI = getURI(this); + + // Related items are bidirectional, so include any pointing to this object + let objects = yield Zotero.Relations.getByPredicateAndObject( + Zotero.Relations.relatedItemPredicate, objectURI + ); + for (let i = 0; i < objects.length; i++) { + addRel(Zotero.Relations.relatedItemPredicate, getURI(objects[i])); + } + + // Also include any owl:sameAs relations pointing to this object + objects = yield Zotero.Relations.getByPredicateAndObject( + Zotero.Relations.linkedObjectPredicate, objectURI + ); + for (let i = 0; i < objects.length; i++) { + addRel(Zotero.Relations.linkedObjectPredicate, getURI(objects[i])); + } + }*/ + + // Relations are stored as predicate-object pairs + obj._relations = this.flattenRelations(relations); + obj._loaded.relations = true; + obj._clearChanged('relations'); + }.bind(this); + + yield Zotero.DB.queryAsync( + sql, + params, + { + noCache: ids.length != 1, + onRow: function (row) { + let id = row.getResultByIndex(0); + + if (lastID && id !== lastID) { + setRows(lastID, rows); + rows = []; + } + + lastID = id; + let predicate = row.getResultByIndex(1); + // No relations + if (predicate === null) { + return; + } + rows.push({ + predicate, + object: row.getResultByIndex(2) + }); + }.bind(this) + } + ); + + if (lastID) { + setRows(lastID, rows); + } +}); + + +/** + * Flatten API JSON relations object into an array of unique predicate-object pairs + * + * @param {Object} relations - Relations object in API JSON format, with predicates as keys + * and arrays of URIs as objects + * @return {Array[]} - Predicate-object pairs + */ +Zotero.DataObjects.prototype.flattenRelations = function (relations) { + var relationsFlat = []; + for (let predicate in relations) { + let object = relations[predicate]; + if (Array.isArray(object)) { + object = Zotero.Utilities.arrayUnique(object); + for (let i = 0; i < object.length; i++) { + relationsFlat.push([predicate, object[i]]); + } + } + else if (typeof object == 'string') { + relationsFlat.push([predicate, object]); + } + else { + Zotero.debug(object, 1); + throw new Error("Invalid relation value"); + } + } + return relationsFlat; +} + + +/** * Reload loaded data of loaded objects * * @param {Array|Number} ids - An id or array of ids @@ -557,25 +805,23 @@ Zotero.DataObjects.prototype.erase = Zotero.Promise.coroutine(function* (ids, op }); - - - +// TEMP: remove Zotero.DataObjects.prototype._load = Zotero.Promise.coroutine(function* (libraryID, ids, options) { var loaded = {}; - + // If library isn't an integer (presumably false or null), skip it if (parseInt(libraryID) != libraryID) { libraryID = false; } - + if (libraryID === false && !ids) { throw new Error("Either libraryID or ids must be provided"); } - + if (libraryID !== false && this._loadedLibraries[libraryID]) { return loaded; } - + var sql = this.primaryDataSQL; var params = []; if (libraryID !== false) { @@ -585,7 +831,7 @@ Zotero.DataObjects.prototype._load = Zotero.Promise.coroutine(function* (library if (ids) { sql += ' AND O.' + this._ZDO_id + ' IN (' + ids.join(',') + ')'; } - + var t = new Date(); yield Zotero.DB.queryAsync( sql, @@ -599,7 +845,7 @@ Zotero.DataObjects.prototype._load = Zotero.Promise.coroutine(function* (library rowObj[columns[i]] = row.getResultByIndex(i); } var obj; - + // Existing object -- reload in place if (this._objectCache[id]) { this._objectCache[id].loadFromRow(rowObj, true); @@ -618,10 +864,10 @@ Zotero.DataObjects.prototype._load = Zotero.Promise.coroutine(function* (library } ); Zotero.debug("Loaded " + this._ZDO_objects + " in " + ((new Date) - t) + "ms"); - + if (!ids) { this._loadedLibraries[libraryID] = true; - + // If loading all objects, remove cached objects that no longer exist for (let i in this._objectCache) { let obj = this._objectCache[i]; @@ -632,15 +878,17 @@ Zotero.DataObjects.prototype._load = Zotero.Promise.coroutine(function* (library this.unload(obj.id); } } - + if (this._postLoad) { this._postLoad(libraryID, ids); } } - + return loaded; }); + + Zotero.DataObjects.prototype._getObjectForRow = function(row) { return new Zotero[this._ZDO_Object]; }; diff --git a/chrome/content/zotero/xpcom/data/item.js b/chrome/content/zotero/xpcom/data/item.js @@ -50,6 +50,9 @@ Zotero.Item = function(itemTypeOrID) { this._attachmentLinkMode = null; this._attachmentContentType = null; this._attachmentPath = null; + this._attachmentSyncState = 0; + this._attachmentSyncedModificationTime = null; + this._attachmentSyncedHash = null; // loadCreators this._creators = []; @@ -90,9 +93,9 @@ Zotero.defineProperty(Zotero.Item.prototype, 'ContainerObjectsClass', { }); Zotero.Item.prototype._dataTypes = Zotero.Item._super.prototype._dataTypes.concat([ + 'creators', 'itemData', 'note', - 'creators', 'childItems', // 'relatedItems', // TODO: remove 'tags', @@ -327,12 +330,14 @@ Zotero.Item.prototype._parseRowData = function(row) { //Zotero.debug("Setting field '" + col + "' to '" + val + "' for item " + this.id); switch (col) { - // Skip + // Unchanged case 'libraryID': case 'itemTypeID': + case 'attachmentSyncState': + case 'attachmentSyncedHash': + case 'attachmentSyncedModificationTime': break; - // Unchanged case 'itemID': col = 'id'; break; @@ -658,10 +663,16 @@ Zotero.Item.prototype.setField = function(field, value, loadIn) { switch (field) { case 'itemTypeID': - case 'dateAdded': break; + case 'dateAdded': case 'dateModified': + // Accept ISO dates + if (Zotero.Date.isISODate(value)) { + let d = Zotero.Date.isoToDate(value); + value = Zotero.Date.dateToSQL(d, true); + } + // Make sure it's valid let date = Zotero.Date.sqlToDate(value, true); if (!date) throw new Error("Invalid SQL date: " + value); @@ -785,11 +796,18 @@ Zotero.Item.prototype.setField = function(field, value, loadIn) { } // Validate access date else if (fieldID == Zotero.ItemFields.getID('accessDate')) { - if (value && (!Zotero.Date.isSQLDate(value) && - !Zotero.Date.isSQLDateTime(value) && - value != 'CURRENT_TIMESTAMP')) { - Zotero.debug("Discarding invalid accessDate '" + value + "' in Item.setField()"); - return false; + if (value && value != 'CURRENT_TIMESTAMP') { + // Accept ISO dates + if (Zotero.Date.isISODate(value)) { + let d = Zotero.Date.isoToDate(value); + value = Zotero.Date.dateToSQL(d, true); + } + + if (!Zotero.Date.isSQLDate(value) && !Zotero.Date.isSQLDateTime(value)) { + Zotero.logError(`Discarding invalid ${field} '${value}' for ` + + `item ${this.libraryKey} in setField()`); + return false; + } } } @@ -831,6 +849,105 @@ Zotero.Item.prototype.getDisplayTitle = function (includeAuthorAndDate) { } +/** + * Update the generated display title from the loaded data + */ +Zotero.Item.prototype.updateDisplayTitle = function () { + var title = this.getField('title', false, true); + var itemTypeID = this.itemTypeID; + var itemTypeName = Zotero.ItemTypes.getName(itemTypeID); + + if (title === "" && (itemTypeID == 8 || itemTypeID == 10)) { // 'letter' and 'interview' itemTypeIDs + var creatorsData = this.getCreators(); + var authors = []; + var participants = []; + for (let i=0; i<creatorsData.length; i++) { + let creatorData = creatorsData[i]; + let creatorTypeID = creatorsData[i].creatorTypeID; + if ((itemTypeID == 8 && creatorTypeID == 16) || // 'letter' + (itemTypeID == 10 && creatorTypeID == 7)) { // 'interview' + participants.push(creatorData); + } + else if ((itemTypeID == 8 && creatorTypeID == 1) || // 'letter'/'author' + (itemTypeID == 10 && creatorTypeID == 6)) { // 'interview'/'interviewee' + authors.push(creatorData); + } + } + + var strParts = []; + if (participants.length > 0) { + let names = []; + let max = Math.min(4, participants.length); + for (let i=0; i<max; i++) { + names.push( + participants[i].name !== undefined + ? participants[i].name + : participants[i].lastName + ); + } + switch (names.length) { + case 1: + var str = 'oneParticipant'; + break; + + case 2: + var str = 'twoParticipants'; + break; + + case 3: + var str = 'threeParticipants'; + break; + + default: + var str = 'manyParticipants'; + } + strParts.push(Zotero.getString('pane.items.' + itemTypeName + '.' + str, names)); + } + else { + strParts.push(Zotero.ItemTypes.getLocalizedString(itemTypeID)); + } + + title = '[' + strParts.join('; ') + ']'; + } + else if (itemTypeID == 17) { // 'case' itemTypeID + if (title) { // common law cases always have case names + var reporter = this.getField('reporter'); + if (reporter) { + title = title + ' (' + reporter + ')'; + } else { + var court = this.getField('court'); + if (court) { + title = title + ' (' + court + ')'; + } + } + } + else { // civil law cases have only shortTitle as case name + var strParts = []; + var caseinfo = ""; + + var part = this.getField('court'); + if (part) { + strParts.push(part); + } + + part = Zotero.Date.multipartToSQL(this.getField('date', true, true)); + if (part) { + strParts.push(part); + } + + var creatorData = this.getCreator(0); + if (creatorData && creatorData.creatorTypeID === 1) { // author + strParts.push(creatorData.lastName); + } + + title = '[' + strParts.join(', ') + ']'; + } + } + + this._displayTitle = title; +}; + + /* * Returns the number of creators for this item */ @@ -1318,9 +1435,7 @@ Zotero.Item.prototype._saveData = Zotero.Promise.coroutine(function* (env) { this.libraryID, parentItemKey ); for (let i=0; i<changedCollections.length; i++) { - yield parentItem.loadCollections(); parentItem.addToCollection(changedCollections[i]); - yield this.loadCollections(); this.removeFromCollection(changedCollections[i]); Zotero.Notifier.queue( @@ -1452,14 +1567,19 @@ Zotero.Item.prototype._saveData = Zotero.Promise.coroutine(function* (env) { } if (this._changed.attachmentData) { - let sql = "REPLACE INTO itemAttachments (itemID, parentItemID, linkMode, " - + "contentType, charsetID, path) VALUES (?,?,?,?,?,?)"; + let sql = "REPLACE INTO itemAttachments " + + "(itemID, parentItemID, linkMode, contentType, charsetID, path, " + + "syncState, storageModTime, storageHash) " + + "VALUES (?,?,?,?,?,?,?,?,?)"; let linkMode = this.attachmentLinkMode; let contentType = this.attachmentContentType; let charsetID = this.attachmentCharset ? Zotero.CharacterSets.getID(this.attachmentCharset) : null; let path = this.attachmentPath; + let syncState = this.attachmentSyncState; + let storageModTime = this.attachmentSyncedModificationTime; + let storageHash = this.attachmentSyncedHash; if (linkMode == Zotero.Attachments.LINK_MODE_LINKED_FILE && libraryType != 'user') { throw new Error("Linked files can only be added to user library"); @@ -1471,7 +1591,10 @@ Zotero.Item.prototype._saveData = Zotero.Promise.coroutine(function* (env) { { int: linkMode }, contentType ? { string: contentType } : null, charsetID ? { int: charsetID } : null, - path ? { string: path } : null + path ? { string: path } : null, + syncState !== undefined ? syncState : 0, + storageModTime !== undefined ? storageModTime : null, + storageHash || null ]; yield Zotero.DB.queryAsync(sql, params); @@ -1812,7 +1935,9 @@ Zotero.Item.prototype.setNote = function(text) { this._hasNote = text !== ''; this._noteText = text; this._noteTitle = Zotero.Notes.noteToTitle(text); - this._displayTitle = this._noteTitle; + if (this.isNote()) { + this._displayTitle = this._noteTitle; + } this._markFieldChange('note', oldText); this._changed.note = true; @@ -2296,10 +2421,9 @@ Zotero.Item.prototype.renameAttachmentFile = Zotero.Promise.coroutine(function* yield this.relinkAttachmentFile(destPath); - yield Zotero.DB.executeTransaction(function* () { - yield Zotero.Sync.Storage.Local.setSyncedHash(this.id, null, false); - yield Zotero.Sync.Storage.Local.setSyncState(this.id, "to_upload"); - }.bind(this)); + this.attachmentSyncedHash = null; + this.attachmentSyncState = "to_upload"; + yield this.saveTx({ skipAll: true }); return true; } @@ -2680,7 +2804,11 @@ Zotero.defineProperty(Zotero.Item.prototype, 'attachmentSyncState', { }, set: function(val) { if (!this.isAttachment()) { - throw ("attachmentSyncState can only be set for attachment items"); + throw new Error("attachmentSyncState can only be set for attachment items"); + } + + if (typeof val == 'string') { + val = Zotero.Sync.Storage.Local["SYNC_STATE_" + val.toUpperCase()]; } switch (this.attachmentLinkMode) { @@ -2689,8 +2817,7 @@ Zotero.defineProperty(Zotero.Item.prototype, 'attachmentSyncState', { break; default: - throw ("attachmentSyncState can only be set for snapshots and " - + "imported files"); + throw new Error("attachmentSyncState can only be set for stored files"); } switch (val) { @@ -2703,8 +2830,7 @@ Zotero.defineProperty(Zotero.Item.prototype, 'attachmentSyncState', { break; default: - throw ("Invalid sync state '" + val - + "' in Zotero.Item.attachmentSyncState setter"); + throw new Error("Invalid sync state '" + val + "'"); } if (val == this.attachmentSyncState) { @@ -2720,6 +2846,85 @@ Zotero.defineProperty(Zotero.Item.prototype, 'attachmentSyncState', { }); +Zotero.defineProperty(Zotero.Item.prototype, 'attachmentSyncedModificationTime', { + get: function () { + if (!this.isFileAttachment()) { + return undefined; + } + return this._attachmentSyncedModificationTime; + }, + set: function (val) { + if (!this.isAttachment()) { + throw ("attachmentSyncedModificationTime can only be set for attachment items"); + } + + switch (this.attachmentLinkMode) { + case Zotero.Attachments.LINK_MODE_IMPORTED_URL: + case Zotero.Attachments.LINK_MODE_IMPORTED_FILE: + break; + + default: + throw new Error("attachmentSyncedModificationTime can only be set for stored files"); + } + + if (typeof val != 'number') { + throw new Error("attachmentSyncedModificationTime must be a number"); + } + if (parseInt(val) != val || val < 0) { + throw new Error("attachmentSyncedModificationTime must be a timestamp in milliseconds"); + } + + if (val == this._attachmentSyncedModificationTime) { + return; + } + + if (!this._changed.attachmentData) { + this._changed.attachmentData = {}; + } + this._changed.attachmentData.syncedModificationTime = true; + this._attachmentSyncedModificationTime = val; + } +}); + + +Zotero.defineProperty(Zotero.Item.prototype, 'attachmentSyncedHash', { + get: function () { + if (!this.isFileAttachment()) { + return undefined; + } + return this._attachmentSyncedHash; + }, + set: function (val) { + if (!this.isAttachment()) { + throw ("attachmentSyncedHash can only be set for attachment items"); + } + + switch (this.attachmentLinkMode) { + case Zotero.Attachments.LINK_MODE_IMPORTED_URL: + case Zotero.Attachments.LINK_MODE_IMPORTED_FILE: + break; + + default: + throw new Error("attachmentSyncedHash can only be set for stored files"); + } + + if (val !== null && val.length != 32) { + throw new Error("Invalid attachment hash '" + val + "'"); + } + + if (val == this._attachmentSyncedHash) { + return; + } + + if (!this._changed.attachmentData) { + this._changed.attachmentData = {}; + } + this._changed.attachmentData.syncedHash = true; + this._attachmentSyncedHash = val; + } +}); + + /** * Modification time of an attachment file * @@ -2784,6 +2989,7 @@ Zotero.defineProperty(Zotero.Item.prototype, 'attachmentHash', { }); + /** * Return plain text of attachment content * @@ -3337,13 +3543,12 @@ Zotero.Item.prototype.getImageSrcWithTags = Zotero.Promise.coroutine(function* ( var uri = this.getImageSrc(); // TODO: Optimize this. Maybe load color/item associations in batch in cacheFields? - yield this.loadTags(); var tags = this.getTags(); if (!tags.length) { return uri; } - var tagColors = yield Zotero.Tags.getColors(this.libraryID); + var tagColors = Zotero.Tags.getColors(this.libraryID); var colorData = []; for (let i=0; i<tags.length; i++) { let tag = tags[i]; @@ -3512,34 +3717,30 @@ Zotero.Item.prototype.diff = function (item, includeMatches, ignoreFields) { * * Currently compares only item data, not primary fields */ -Zotero.Item.prototype.multiDiff = Zotero.Promise.coroutine(function* (otherItems, ignoreFields) { - var thisData = yield this.toJSON(); +Zotero.Item.prototype.multiDiff = function (otherItems, ignoreFields) { + var thisData = this.toJSON(); var alternatives = {}; var hasDiffs = false; for (let i = 0; i < otherItems.length; i++) { - let otherItem = otherItems[i]; - let diff = []; - let otherData = yield otherItem.toJSON(); - let numDiffs = this.ObjectsClass.diff(thisData, otherData, diff); - - if (numDiffs) { - for (let field in diff[1]) { - if (ignoreFields && ignoreFields.indexOf(field) != -1) { - continue; - } - - var value = diff[1][field]; - - if (!alternatives[field]) { - hasDiffs = true; - alternatives[field] = [value]; - } - else if (alternatives[field].indexOf(value) == -1) { - hasDiffs = true; - alternatives[field].push(value); - } + let otherData = otherItems[i].toJSON(); + let changeset = Zotero.DataObjectUtilities.diff(thisData, otherData, ignoreFields); + + for (let i = 0; i < changeset.length; i++) { + let change = changeset[i]; + + if (change.op == 'delete') { + continue; + } + + if (!alternatives[change.field]) { + hasDiffs = true; + alternatives[change.field] = [change.value]; + } + else if (alternatives[change.field].indexOf(change.value) == -1) { + hasDiffs = true; + alternatives[change.field].push(change.value); } } } @@ -3549,7 +3750,7 @@ Zotero.Item.prototype.multiDiff = Zotero.Promise.coroutine(function* (otherItems } return alternatives; -}); +}; /** @@ -3561,15 +3762,13 @@ Zotero.Item.prototype.multiDiff = Zotero.Promise.coroutine(function* (otherItems * @param {Boolean} [skipTags=false] - Skip tags * @return {Promise<Zotero.Item>} */ -Zotero.Item.prototype.clone = Zotero.Promise.coroutine(function* (libraryID, skipTags) { +Zotero.Item.prototype.clone = function (libraryID, skipTags) { Zotero.debug('Cloning item ' + this.id); if (libraryID !== undefined && libraryID !== null && typeof libraryID !== 'number') { throw new Error("libraryID must be null or an integer"); } - yield this.loadPrimaryData(); - if (libraryID === undefined || libraryID === null) { libraryID = this.libraryID; } @@ -3579,7 +3778,6 @@ Zotero.Item.prototype.clone = Zotero.Promise.coroutine(function* (libraryID, ski newItem.libraryID = libraryID; newItem.setType(this.itemTypeID); - yield this.loadItemData(); var fieldIDs = this.getUsedFields(); for (let i = 0; i < fieldIDs.length; i++) { let fieldID = fieldIDs[i]; @@ -3588,11 +3786,9 @@ Zotero.Item.prototype.clone = Zotero.Promise.coroutine(function* (libraryID, ski // Regular item if (this.isRegularItem()) { - yield this.loadCreators(); newItem.setCreators(this.getCreators()); } else { - yield this.loadNote(); newItem.setNote(this.getNote()); if (sameLibrary) { var parent = this.parentKey; @@ -3614,29 +3810,16 @@ Zotero.Item.prototype.clone = Zotero.Promise.coroutine(function* (libraryID, ski } if (!skipTags) { - yield this.loadTags(); newItem.setTags(this.getTags()); } if (sameLibrary) { // DEBUG: this will add reverse-only relateds too - yield this.loadRelations(); newItem.setRelations(this.getRelations()); } return newItem; -}); - - -/** - * @return {Promise<Zotero.Item>} - A copy of the item with primary data loaded - */ -Zotero.Item.prototype.copy = Zotero.Promise.coroutine(function* () { - var newItem = new Zotero.Item; - newItem.id = this.id; - yield newItem.loadPrimaryData(); - return newItem; -});; +} Zotero.Item.prototype._eraseData = Zotero.Promise.coroutine(function* (env) { @@ -3721,8 +3904,6 @@ Zotero.Item.prototype.isCollection = function() { /** * Populate the object's data from an API JSON data object - * - * If this object is identified (has an id or library/key), loadAllData() must have been called. */ Zotero.Item.prototype.fromJSON = function (json) { if (!json.itemType && !this._itemTypeID) { @@ -3867,7 +4048,7 @@ Zotero.Item.prototype.fromJSON = function (json) { /** * @param {Object} options */ -Zotero.Item.prototype.toJSON = Zotero.Promise.coroutine(function* (options = {}) { +Zotero.Item.prototype.toJSON = function (options = {}) { var env = this._preToJSON(options); var mode = env.mode; @@ -3877,7 +4058,6 @@ Zotero.Item.prototype.toJSON = Zotero.Promise.coroutine(function* (options = {}) obj.itemType = Zotero.ItemTypes.getName(this.itemTypeID); // Fields - yield this.loadItemData(); for (let i in this._itemData) { let val = this.getField(i) + ''; if (val !== '' || mode == 'full') { @@ -3887,7 +4067,6 @@ Zotero.Item.prototype.toJSON = Zotero.Promise.coroutine(function* (options = {}) // Creators if (this.isRegularItem()) { - yield this.loadCreators() obj.creators = this.getCreatorsJSON(); } else { @@ -3912,18 +4091,18 @@ Zotero.Item.prototype.toJSON = Zotero.Promise.coroutine(function* (options = {}) if (this.isFileAttachment()) { if (options.syncedStorageProperties) { - obj.mtime = yield Zotero.Sync.Storage.Local.getSyncedModificationTime(this.id); - obj.md5 = yield Zotero.Sync.Storage.Local.getSyncedHash(this.id); + obj.mtime = this.attachmentSyncedModificationTime; + obj.md5 = this.attachmentSyncedHash; } else { - obj.mtime = (yield this.attachmentModificationTime) || null; - obj.md5 = (yield this.attachmentHash) || null; + // TEMP + //obj.mtime = (yield this.attachmentModificationTime) || null; + //obj.md5 = (yield this.attachmentHash) || null; } } } // Notes and embedded attachment notes - yield this.loadNote(); let note = this.getNote(); if (note !== "" || mode == 'full' || (mode == 'new' && this.isNote())) { obj.note = note; @@ -3932,7 +4111,6 @@ Zotero.Item.prototype.toJSON = Zotero.Promise.coroutine(function* (options = {}) // Tags obj.tags = []; - yield this.loadTags() var tags = this.getTags(); for (let i=0; i<tags.length; i++) { obj.tags.push(tags[i]); @@ -3940,14 +4118,12 @@ Zotero.Item.prototype.toJSON = Zotero.Promise.coroutine(function* (options = {}) // Collections if (this.isTopLevelItem()) { - yield this.loadCollections(); obj.collections = this.getCollections().map(function (id) { return this.ContainerObjectsClass.getLibraryAndKeyFromID(id).key; }.bind(this)); } // Relations - yield this.loadRelations(); obj.relations = this.getRelations() // Deleted @@ -3956,16 +4132,21 @@ Zotero.Item.prototype.toJSON = Zotero.Promise.coroutine(function* (options = {}) obj.deleted = deleted ? 1 : 0; } - obj.dateAdded = Zotero.Date.sqlToISO8601(this.dateAdded); - obj.dateModified = Zotero.Date.sqlToISO8601(this.dateModified); if (obj.accessDate) obj.accessDate = Zotero.Date.sqlToISO8601(obj.accessDate); + if (this.dateAdded) { + obj.dateAdded = Zotero.Date.sqlToISO8601(this.dateAdded); + } + if (this.dateModified) { + obj.dateModified = Zotero.Date.sqlToISO8601(this.dateModified); + } + return this._postToJSON(env); -}); +} -Zotero.Item.prototype.toResponseJSON = Zotero.Promise.coroutine(function* (options = {}) { - var json = yield this.constructor._super.prototype.toResponseJSON.apply(this, options); +Zotero.Item.prototype.toResponseJSON = function (options = {}) { + var json = this.constructor._super.prototype.toResponseJSON.apply(this, options); // creatorSummary var firstCreator = this.getField('firstCreator'); @@ -3983,7 +4164,7 @@ Zotero.Item.prototype.toResponseJSON = Zotero.Promise.coroutine(function* (optio json.meta.numChildren = this.numChildren(); } return json; -}) +}; ////////////////////////////////////////////////////////////////////////////// @@ -3992,352 +4173,6 @@ Zotero.Item.prototype.toResponseJSON = Zotero.Promise.coroutine(function* (optio // ////////////////////////////////////////////////////////////////////////////// -/* - * Load in the field data from the database - */ -Zotero.Item.prototype.loadItemData = Zotero.Promise.coroutine(function* (reload) { - if (this._loaded.itemData && !reload) { - return; - } - - Zotero.debug("Loading item data for item " + this.libraryKey); - - if (!this.id) { - throw new Error('ItemID not set for object before attempting to load data'); - } - - if (!this.isNote()) { - var sql = "SELECT fieldID, value FROM itemData NATURAL JOIN itemDataValues WHERE itemID=?"; - yield Zotero.DB.queryAsync( - sql, - this.id, - { - onRow: function (row) { - this.setField(row.getResultByIndex(0), row.getResultByIndex(1), true); - }.bind(this) - } - ); - - // Mark nonexistent fields as loaded - let itemTypeFields = Zotero.ItemFields.getItemTypeFields(this.itemTypeID); - for (let i=0; i<itemTypeFields.length; i++) { - let fieldID = itemTypeFields[i]; - if (this._itemData[fieldID] === null) { - this._itemData[fieldID] = false; - } - } - } - - if (this.isNote() || this.isAttachment()) { - var sql = "SELECT title FROM itemNotes WHERE itemID=?"; - var row = yield Zotero.DB.rowQueryAsync(sql, this.id); - if (row) { - let title = row.title; - this._noteTitle = title !== false ? title : ''; - } - } - - this._loaded.itemData = true; - this._clearChanged('itemData'); - yield this.loadDisplayTitle(reload); -}); - - -Zotero.Item.prototype.loadNote = Zotero.Promise.coroutine(function* (reload) { - if (this._loaded.note && !reload) { - return; - } - - if (!this.isNote() && !this.isAttachment()) { - throw new Error("Can only load note for note or attachment item"); - } - - Zotero.debug("Loading note data for item " + this.libraryKey); - - var sql = "SELECT note FROM itemNotes WHERE itemID=?"; - var row = yield Zotero.DB.rowQueryAsync(sql, this.id); - if (row) { - let note = row.note; - - // Convert non-HTML notes on-the-fly - if (note !== "") { - if (!note.substr(0, 36).match(/^<div class="zotero-note znv[0-9]+">/)) { - note = Zotero.Utilities.htmlSpecialChars(note); - note = Zotero.Notes.notePrefix + '<p>' - + note.replace(/\n/g, '</p><p>') - .replace(/\t/g, '&nbsp;&nbsp;&nbsp;&nbsp;') - .replace(/ /g, '&nbsp;&nbsp;') - + '</p>' + Zotero.Notes.noteSuffix; - note = note.replace(/<p>\s*<\/p>/g, '<p>&nbsp;</p>'); - let sql = "UPDATE itemNotes SET note=? WHERE itemID=?"; - yield Zotero.DB.queryAsync(sql, [note, this.id]); - } - - // Don't include <div> wrapper when returning value - let startLen = note.substr(0, 36).match(/^<div class="zotero-note znv[0-9]+">/)[0].length; - let endLen = 6; // "</div>".length - note = note.substr(startLen, note.length - startLen - endLen); - } - - this._noteText = note ? note : ''; - } - - this._loaded.note = true; - this._clearChanged('note'); -}); - - -Zotero.Item.prototype.loadDisplayTitle = Zotero.Promise.coroutine(function* (reload) { - if (this._displayTitle !== null && !reload) { - return; - } - - var title = this.getField('title', false, true); - var itemTypeID = this.itemTypeID; - var itemTypeName = Zotero.ItemTypes.getName(itemTypeID); - - if (title === "" && (itemTypeID == 8 || itemTypeID == 10)) { // 'letter' and 'interview' itemTypeIDs - yield this.loadCreators(); - var creatorsData = this.getCreators(); - var authors = []; - var participants = []; - for (let i=0; i<creatorsData.length; i++) { - let creatorData = creatorsData[i]; - let creatorTypeID = creatorsData[i].creatorTypeID; - if ((itemTypeID == 8 && creatorTypeID == 16) || // 'letter' - (itemTypeID == 10 && creatorTypeID == 7)) { // 'interview' - participants.push(creatorData); - } - else if ((itemTypeID == 8 && creatorTypeID == 1) || // 'letter'/'author' - (itemTypeID == 10 && creatorTypeID == 6)) { // 'interview'/'interviewee' - authors.push(creatorData); - } - } - - var strParts = []; - if (participants.length > 0) { - let names = []; - let max = Math.min(4, participants.length); - for (let i=0; i<max; i++) { - names.push( - participants[i].name !== undefined - ? participants[i].name - : participants[i].lastName - ); - } - switch (names.length) { - case 1: - var str = 'oneParticipant'; - break; - - case 2: - var str = 'twoParticipants'; - break; - - case 3: - var str = 'threeParticipants'; - break; - - default: - var str = 'manyParticipants'; - } - strParts.push(Zotero.getString('pane.items.' + itemTypeName + '.' + str, names)); - } - else { - strParts.push(Zotero.ItemTypes.getLocalizedString(itemTypeID)); - } - - title = '[' + strParts.join('; ') + ']'; - } - else if (itemTypeID == 17) { // 'case' itemTypeID - if (title) { // common law cases always have case names - var reporter = this.getField('reporter'); - if (reporter) { - title = title + ' (' + reporter + ')'; - } else { - var court = this.getField('court'); - if (court) { - title = title + ' (' + court + ')'; - } - } - } - else { // civil law cases have only shortTitle as case name - var strParts = []; - var caseinfo = ""; - - var part = this.getField('court'); - if (part) { - strParts.push(part); - } - - part = Zotero.Date.multipartToSQL(this.getField('date', true, true)); - if (part) { - strParts.push(part); - } - - yield this.loadCreators() - var creatorData = this.getCreator(0); - if (creatorData && creatorData.creatorTypeID === 1) { // author - strParts.push(creatorData.lastName); - } - - title = '[' + strParts.join(', ') + ']'; - } - } - - return this._displayTitle = title; -}); - - -/* - * Load in the creators from the database - */ -Zotero.Item.prototype.loadCreators = Zotero.Promise.coroutine(function* (reload) { - if (this._loaded.creators && !reload) { - return; - } - - Zotero.debug("Loading creators for item " + this.libraryKey); - - if (!this.id) { - throw new Error('ItemID not set for item before attempting to load creators'); - } - - var sql = 'SELECT creatorID, creatorTypeID, orderIndex FROM itemCreators ' - + 'WHERE itemID=? ORDER BY orderIndex'; - var rows = yield Zotero.DB.queryAsync(sql, this.id); - - this._creators = []; - this._creatorIDs = []; - this._loaded.creators = true; - this._clearChanged('creators'); - - if (!rows) { - return true; - } - - var maxOrderIndex = -1; - for (var i=0; i<rows.length; i++) { - let row = rows[i]; - if (row.orderIndex > maxOrderIndex) { - maxOrderIndex = row.orderIndex; - } - let creatorData = yield Zotero.Creators.getAsync(row.creatorID); - creatorData.creatorTypeID = row.creatorTypeID; - this._creators[i] = creatorData; - this._creatorIDs[i] = row.creatorID; - } - if (i <= maxOrderIndex) { - Zotero.debug("Fixing incorrect creator indexes for item " + this.libraryKey - + " (" + i + ", " + maxOrderIndex + ")", 2); - while (i <= maxOrderIndex) { - this._changed.creators[i] = true; - i++; - } - } - - return true; -}); - - -Zotero.Item.prototype.loadChildItems = Zotero.Promise.coroutine(function* (reload) { - if (this._loaded.childItems && !reload) { - return; - } - - if (this.isNote() || this.isAttachment()) { - return; - } - - // Attachments - this._attachments = { - rows: null, - chronologicalWithTrashed: null, - chronologicalWithoutTrashed: null, - alphabeticalWithTrashed: null, - alphabeticalWithoutTrashed: null - }; - var sql = "SELECT A.itemID, value AS title, CASE WHEN DI.itemID IS NULL THEN 0 ELSE 1 END AS trashed " - + "FROM itemAttachments A " - + "NATURAL JOIN items I " - + "LEFT JOIN itemData ID ON (fieldID=110 AND A.itemID=ID.itemID) " - + "LEFT JOIN itemDataValues IDV USING (valueID) " - + "LEFT JOIN deletedItems DI USING (itemID) " - + "WHERE parentItemID=?"; - // Since we do the sort here and cache these results, a restart will be required - // if this pref (off by default) is turned on, but that's OK - if (Zotero.Prefs.get('sortAttachmentsChronologically')) { - sql += " ORDER BY dateAdded"; - } - this._attachments.rows = yield Zotero.DB.queryAsync(sql, this.id); - - // - // Notes - // - this._notes = { - rows: null, - rowsEmbedded: null, - chronologicalWithTrashed: null, - chronologicalWithoutTrashed: null, - alphabeticalWithTrashed: null, - alphabeticalWithoutTrashed: null, - numWithTrashed: null, - numWithoutTrashed: null, - numWithTrashedWithEmbedded: null, - numWithoutTrashedWithoutEmbedded: null - }; - var sql = "SELECT N.itemID, title, CASE WHEN DI.itemID IS NULL THEN 0 ELSE 1 END AS trashed " - + "FROM itemNotes N " - + "NATURAL JOIN items I " - + "LEFT JOIN deletedItems DI USING (itemID) " - + "WHERE parentItemID=?"; - if (Zotero.Prefs.get('sortAttachmentsChronologically')) { - sql += " ORDER BY dateAdded"; - } - this._notes.rows = yield Zotero.DB.queryAsync(sql, this.id); - - this._loaded.childItems = true; - this._clearChanged('childItems'); -}); - - -Zotero.Item.prototype.loadTags = Zotero.Promise.coroutine(function* (reload) { - if (this._loaded.tags && !reload) { - return; - } - - if (!this._id) { - return; - } - var sql = "SELECT tagID AS id, name AS tag, type FROM itemTags " - + "JOIN tags USING (tagID) WHERE itemID=?"; - var rows = yield Zotero.DB.queryAsync(sql, this.id); - - this._tags = []; - for (let i=0; i<rows.length; i++) { - let row = rows[i]; - this._tags.push(Zotero.Tags.cleanData(row)); - } - - this._loaded.tags = true; - this._clearChanged('tags'); -}); - - - -Zotero.Item.prototype.loadCollections = Zotero.Promise.coroutine(function* (reload) { - if (this._loaded.collections && !reload) { - return; - } - if (!this._id) { - return; - } - var sql = "SELECT collectionID FROM collectionItems WHERE itemID=?"; - this._collections = yield Zotero.DB.columnQueryAsync(sql, this.id); - this._loaded.collections = true; - this._clearChanged('collections'); -}); - /** * Return an item in the specified library equivalent to this item diff --git a/chrome/content/zotero/xpcom/data/items.js b/chrome/content/zotero/xpcom/data/items.js @@ -84,7 +84,10 @@ Zotero.Items = function() { attachmentCharset: "CS.charset AS attachmentCharset", attachmentLinkMode: "IA.linkMode AS attachmentLinkMode", attachmentContentType: "IA.contentType AS attachmentContentType", - attachmentPath: "IA.path AS attachmentPath" + attachmentPath: "IA.path AS attachmentPath", + attachmentSyncState: "IA.syncState AS attachmentSyncState", + attachmentSyncedModificationTime: "IA.storageModTime AS attachmentSyncedModificationTime", + attachmentSyncedHash: "IA.storageHash AS attachmentSyncedHash" }; } }, {lazy: true}); @@ -204,7 +207,7 @@ Zotero.Items = function() { for (let i=0; i<ids.length; i++) { let prefix = i > 0 ? ',\n' : ''; let item = yield this.getAsync(ids[i], { noCache: true }); - var json = yield item.toResponseJSON(); + var json = item.toResponseJSON(); yield prefix + JSON.stringify(json, null, 4); } @@ -212,195 +215,507 @@ Zotero.Items = function() { }; - this._cachedFields = {}; - this.cacheFields = Zotero.Promise.coroutine(function* (libraryID, fields, items) { - if (items && items.length == 0) { - return; - } + // + // Bulk data loading functions + // + // These are called by Zotero.DataObjects.prototype._loadDataType(). + // + this._loadItemData = Zotero.Promise.coroutine(function* (libraryID, ids, idSQL) { + var missingItems = {}; + var itemFieldsCached = {}; - var t = new Date; + var sql = "SELECT itemID, fieldID, value FROM items " + + "JOIN itemData USING (itemID) " + + "JOIN itemDataValues USING (valueID) WHERE libraryID=? AND itemTypeID!=?" + idSQL; + var params = [libraryID, Zotero.ItemTypes.getID('note')]; + yield Zotero.DB.queryAsync( + sql, + params, + { + noCache: ids.length != 1, + onRow: function (row) { + let itemID = row.getResultByIndex(0); + let fieldID = row.getResultByIndex(1); + let value = row.getResultByIndex(2); + + //Zotero.debug('Setting field ' + fieldID + ' for item ' + itemID); + if (this._objectCache[itemID]) { + if (value === null) { + value = false; + } + this._objectCache[itemID].setField(fieldID, value, true); + } + else { + if (!missingItems[itemID]) { + missingItems[itemID] = true; + Zotero.logError("itemData row references nonexistent item " + itemID); + } + } + if (!itemFieldsCached[itemID]) { + itemFieldsCached[itemID] = {}; + } + itemFieldsCached[itemID][fieldID] = true; + }.bind(this) + } + ); - fields = fields.concat(); + var sql = "SELECT itemID FROM items WHERE libraryID=?" + idSQL; + var params = [libraryID]; + var allItemIDs = []; + yield Zotero.DB.queryAsync( + sql, + params, + { + noCache: ids.length != 1, + onRow: function (row) { + let itemID = row.getResultByIndex(0); + let item = this._objectCache[itemID]; + + // Set nonexistent fields in the cache list to false (instead of null) + let fieldIDs = Zotero.ItemFields.getItemTypeFields(item.itemTypeID); + for (let j=0; j<fieldIDs.length; j++) { + let fieldID = fieldIDs[j]; + if (!itemFieldsCached[itemID] || !itemFieldsCached[itemID][fieldID]) { + //Zotero.debug('Setting field ' + fieldID + ' to false for item ' + itemID); + item.setField(fieldID, false, true); + } + } + + allItemIDs.push(itemID); + }.bind(this) + } + ); - // Needed for display titles for some item types - if (fields.indexOf('title') != -1) { - fields.push('reporter', 'court'); - } - Zotero.debug("Caching fields [" + fields.join() + "]" - + (items ? " for " + items.length + " items" : '') - + " in library " + libraryID); + var titleFieldID = Zotero.ItemFields.getID('title'); - if (items && items.length > 0) { - yield this._load(libraryID, items); - } - else { - yield this._load(libraryID); - } + // Note titles + var sql = "SELECT itemID, title FROM items JOIN itemNotes USING (itemID) " + + "WHERE libraryID=? AND itemID NOT IN (SELECT itemID FROM itemAttachments)" + idSQL; + var params = [libraryID]; - var primaryFields = []; - var fieldIDs = []; - for each(var field in fields) { - // Check if field already cached - if (this._cachedFields[libraryID] && this._cachedFields[libraryID].indexOf(field) != -1) { - continue; + yield Zotero.DB.queryAsync( + sql, + params, + { + onRow: function (row) { + let itemID = row.getResultByIndex(0); + let title = row.getResultByIndex(1); + + //Zotero.debug('Setting title for note ' + row.itemID); + if (this._objectCache[itemID]) { + this._objectCache[itemID].setField(titleFieldID, title, true); + } + else { + if (!missingItems[itemID]) { + missingItems[itemID] = true; + Zotero.logError("itemData row references nonexistent item " + itemID); + } + } + }.bind(this) } + ); + + for (let i=0; i<allItemIDs.length; i++) { + let itemID = allItemIDs[i]; + let item = this._objectCache[itemID]; - if (!this._cachedFields[libraryID]) { - this._cachedFields[libraryID] = []; - } - this._cachedFields[libraryID].push(field); + // Mark as loaded + item._loaded.itemData = true; + item._clearChanged('itemData'); - if (this.isPrimaryField(field)) { - primaryFields.push(field); - } - else { - fieldIDs.push(Zotero.ItemFields.getID(field)); - if (Zotero.ItemFields.isBaseField(field)) { - fieldIDs = fieldIDs.concat(Zotero.ItemFields.getTypeFieldsFromBase(field)); - } - } + // Display titles + item.updateDisplayTitle() } + }); + + + this._loadCreators = Zotero.Promise.coroutine(function* (libraryID, ids, idSQL) { + var sql = 'SELECT itemID, creatorID, creatorTypeID, orderIndex ' + + 'FROM items LEFT JOIN itemCreators USING (itemID) ' + + 'WHERE libraryID=?' + idSQL + " ORDER BY itemID, orderIndex"; + var params = [libraryID]; + var rows = yield Zotero.DB.queryAsync(sql, params); - if (primaryFields.length) { - var sql = "SELECT O.itemID, " - + primaryFields.map((val) => this.getPrimaryDataSQLPart(val)).join(', ') - + this.primaryDataSQLFrom + " AND O.libraryID=?"; - var params = [libraryID]; - if (items) { - sql += " AND O.itemID IN (" + items.join() + ")"; + // Mark creator indexes above the number of creators as changed, + // so that they're cleared if the item is saved + var fixIncorrectIndexes = function (item, numCreators, maxOrderIndex) { + Zotero.debug("Fixing incorrect creator indexes for item " + item.libraryKey + + " (" + numCreators + ", " + maxOrderIndex + ")", 2); + var i = numCreators; + while (i <= maxOrderIndex) { + item._changed.creators[i] = true; + i++; } - yield Zotero.DB.queryAsync( - sql, - params, - { - onRow: function (row) { - let obj = { - itemID: row.getResultByIndex(0) - }; - for (let i=0; i<primaryFields.length; i++) { - obj[primaryFields[i]] = row.getResultByIndex(i); - } - Zotero.debug(obj.itemID); - Zotero.debug(Object.keys(this._objectCache)); - this._objectCache[obj.itemID].loadFromRow(obj); - }.bind(this) + }; + + var lastItemID; + var item; + var index = 0; + var maxOrderIndex = -1; + for (let i = 0; i < rows.length; i++) { + let row = rows[i]; + let itemID = row.itemID; + + if (itemID != lastItemID) { + if (!this._objectCache[itemID]) { + throw new Error("Item " + itemID + " not loaded"); + } + item = this._objectCache[itemID]; + + item._creators = []; + item._creatorIDs = []; + item._loaded.creators = true; + item._clearChanged('creators'); + + if (!row.creatorID) { + lastItemID = row.itemID; + continue; } - ); + + if (index <= maxOrderIndex) { + fixIncorrectIndexes(item, index, maxOrderIndex); + } + + index = 0; + maxOrderIndex = -1; + } + + lastItemID = row.itemID; + + if (row.orderIndex > maxOrderIndex) { + maxOrderIndex = row.orderIndex; + } + + let creatorData = Zotero.Creators.get(row.creatorID); + creatorData.creatorTypeID = row.creatorTypeID; + item._creators[index] = creatorData; + item._creatorIDs[index] = row.creatorID; + index++; } - // All fields already cached - if (!fieldIDs.length) { - Zotero.debug('All fields already cached'); - return; + if (index <= maxOrderIndex) { + fixIncorrectIndexes(item, index, maxOrderIndex); } + }); + + + this._loadNotes = Zotero.Promise.coroutine(function* (libraryID, ids, idSQL) { + var notesToUpdate = []; - var sql = "SELECT itemID FROM items WHERE libraryID=?"; - var params = [libraryID]; - var allItemIDs = yield Zotero.DB.columnQueryAsync(sql, params); - var itemFieldsCached = {}; - - var sql = "SELECT itemID, fieldID, value FROM items JOIN itemData USING (itemID) " - + "JOIN itemDataValues USING (valueID) WHERE libraryID=?"; + var sql = "SELECT itemID, note FROM items " + + "JOIN itemNotes USING (itemID) " + + "WHERE libraryID=?" + idSQL; var params = [libraryID]; - if (items) { - sql += " AND itemID IN (" + items.join() + ")"; - } - sql += " AND fieldID IN (" + fieldIDs.join() + ")"; yield Zotero.DB.queryAsync( sql, params, { + noCache: ids.length != 1, onRow: function (row) { let itemID = row.getResultByIndex(0); - let fieldID = row.getResultByIndex(1); - let value = row.getResultByIndex(2); - - //Zotero.debug('Setting field ' + fieldID + ' for item ' + itemID); - if (this._objectCache[itemID]) { - this._objectCache[itemID].setField(fieldID, value, true); + let item = this._objectCache[itemID]; + if (!item) { + throw new Error("Item " + itemID + " not found"); } - else { - if (!missingItems) { - var missingItems = {}; - } - if (!missingItems[itemID]) { - missingItems[itemID] = true; - Zotero.debug("itemData row references nonexistent item " + itemID); - Components.utils.reportError("itemData row references nonexistent item " + itemID); + let note = row.getResultByIndex(1); + + // Convert non-HTML notes on-the-fly + if (note !== "") { + if (!note.substr(0, 36).match(/^<div class="zotero-note znv[0-9]+">/)) { + note = Zotero.Utilities.htmlSpecialChars(note); + note = Zotero.Notes.notePrefix + '<p>' + + note.replace(/\n/g, '</p><p>') + .replace(/\t/g, '&nbsp;&nbsp;&nbsp;&nbsp;') + .replace(/ /g, '&nbsp;&nbsp;') + + '</p>' + Zotero.Notes.noteSuffix; + note = note.replace(/<p>\s*<\/p>/g, '<p>&nbsp;</p>'); + notesToUpdate.push([item.id, note]); } + + // Don't include <div> wrapper when returning value + let startLen = note.substr(0, 36).match(/^<div class="zotero-note znv[0-9]+">/)[0].length; + let endLen = 6; // "</div>".length + note = note.substr(startLen, note.length - startLen - endLen); } - if (!itemFieldsCached[itemID]) { - itemFieldsCached[itemID] = {}; - } - itemFieldsCached[itemID][fieldID] = true; + item._noteText = note ? note : ''; + item._loaded.note = true; + item._clearChanged('note'); }.bind(this) } ); - // Set nonexistent fields in the cache list to false (instead of null) - for (let i=0; i<allItemIDs.length; i++) { - let itemID = allItemIDs[i]; - for (let j=0; j<fieldIDs.length; j++) { - let fieldID = fieldIDs[j]; - if (Zotero.ItemFields.isValidForType(fieldID, this._objectCache[itemID].itemTypeID)) { - if (!itemFieldsCached[itemID] || !itemFieldsCached[itemID][fieldID]) { - //Zotero.debug('Setting field ' + fieldID + ' to false for item ' + itemID); - this._objectCache[itemID].setField(fieldID, false, true); + if (notesToUpdate.length) { + yield Zotero.DB.executeTransaction(function* () { + for (let i = 0; i < notesToUpdate.length; i++) { + let row = notesToUpdate[i]; + let sql = "UPDATE itemNotes SET note=? WHERE itemID=?"; + yield Zotero.DB.queryAsync(sql, [row[1], row[0]]); + } + }.bind(this)); + } + + // Mark notes and attachments without notes as loaded + sql = "SELECT itemID FROM items WHERE libraryID=?" + idSQL + + " AND itemTypeID IN (?, ?) AND itemID NOT IN (SELECT itemID FROM itemNotes)"; + params = [libraryID, Zotero.ItemTypes.getID('note'), Zotero.ItemTypes.getID('attachment')]; + yield Zotero.DB.queryAsync( + sql, + params, + { + noCache: ids.length != 1, + onRow: function (row) { + let itemID = row.getResultByIndex(0); + let item = this._objectCache[itemID]; + if (!item) { + throw new Error("Item " + itemID + " not loaded"); } + + item._noteText = ''; + item._loaded.note = true; + item._clearChanged('note'); + }.bind(this) + } + ); + }); + + + this._loadChildItems = Zotero.Promise.coroutine(function* (libraryID, ids, idSQL) { + var params = [libraryID]; + var rows = []; + var onRow = function (row, setFunc) { + var itemID = row.getResultByIndex(0); + + if (lastItemID && itemID !== lastItemID) { + setFunc(lastItemID, rows); + rows = []; + } + + lastItemID = itemID; + rows.push({ + itemID: row.getResultByIndex(1), + title: row.getResultByIndex(2), + trashed: row.getResultByIndex(3) + }); + }; + + var sql = "SELECT parentItemID, A.itemID, value AS title, " + + "CASE WHEN DI.itemID IS NULL THEN 0 ELSE 1 END AS trashed " + + "FROM itemAttachments A " + + "JOIN items I ON (A.parentItemID=I.itemID) " + + "LEFT JOIN itemData ID ON (fieldID=110 AND A.itemID=ID.itemID) " + + "LEFT JOIN itemDataValues IDV USING (valueID) " + + "LEFT JOIN deletedItems DI USING (itemID) " + + "WHERE libraryID=?" + + (ids.length ? " AND parentItemID IN (" + ids.map(id => parseInt(id)).join(", ") + ")" : ""); + // Since we do the sort here and cache these results, a restart will be required + // if this pref (off by default) is turned on, but that's OK + if (Zotero.Prefs.get('sortAttachmentsChronologically')) { + sql += " ORDER BY parentItemID, dateAdded"; + } + var setAttachmentItem = function (itemID, rows) { + var item = this._objectCache[itemID]; + if (!item) { + throw new Error("Item " + itemID + " not loaded"); + } + + item._attachments = { + rows, + chronologicalWithTrashed: null, + chronologicalWithoutTrashed: null, + alphabeticalWithTrashed: null, + alphabeticalWithoutTrashed: null + }; + }.bind(this); + var lastItemID = null; + yield Zotero.DB.queryAsync( + sql, + params, + { + noCache: ids.length != 1, + onRow: function (row) { + onRow(row, setAttachmentItem); } } + ); + if (lastItemID) { + setAttachmentItem(lastItemID, rows); } - // If 'title' is one of the fields, load in display titles (note titles, letter titles...) - if (fields.indexOf('title') != -1) { - var titleFieldID = Zotero.ItemFields.getID('title'); - - // Note titles - var sql = "SELECT itemID, title FROM items JOIN itemNotes USING (itemID) " - + "WHERE libraryID=? AND itemID NOT IN (SELECT itemID FROM itemAttachments)"; - var params = [libraryID]; - if (items) { - sql += " AND itemID IN (" + items.join() + ")"; + // + // Notes + // + sql = "SELECT parentItemID, N.itemID, title, " + + "CASE WHEN DI.itemID IS NULL THEN 0 ELSE 1 END AS trashed " + + "FROM itemNotes N " + + "JOIN items I ON (N.parentItemID=I.itemID) " + + "LEFT JOIN deletedItems DI USING (itemID) " + + "WHERE libraryID=?" + + (ids.length ? " AND parentItemID IN (" + ids.map(id => parseInt(id)).join(", ") + ")" : ""); + if (Zotero.Prefs.get('sortNotesChronologically')) { + sql += " ORDER BY parentItemID, dateAdded"; + } + var setNoteItem = function (itemID, rows) { + var item = this._objectCache[itemID]; + if (!item) { + throw new Error("Item " + itemID + " not loaded"); } - yield Zotero.DB.queryAsync( - sql, - params, - { - onRow: function (row) { - let itemID = row.getResultByIndex(0); - let title = row.getResultByIndex(1); - - //Zotero.debug('Setting title for note ' + row.itemID); - if (this._objectCache[itemID]) { - this._objectCache[itemID].setField(titleFieldID, title, true); - } - else { - if (!missingItems) { - var missingItems = {}; - } - if (!missingItems[itemID]) { - missingItems[itemID] = true; - Components.utils.reportError( - "itemData row references nonexistent item " + itemID - ); - } - } - }.bind(this) + item._notes = { + rows, + rowsEmbedded: null, + chronologicalWithTrashed: null, + chronologicalWithoutTrashed: null, + alphabeticalWithTrashed: null, + alphabeticalWithoutTrashed: null, + numWithTrashed: null, + numWithoutTrashed: null, + numWithTrashedWithEmbedded: null, + numWithoutTrashedWithoutEmbedded: null + }; + }.bind(this); + lastItemID = null; + rows = []; + yield Zotero.DB.queryAsync( + sql, + params, + { + noCache: ids.length != 1, + onRow: function (row) { + onRow(row, setNoteItem); } - ); + } + ); + if (lastItemID) { + setNoteItem(lastItemID, rows); + } + + // Mark all top-level items as having child items loaded + sql = "SELECT itemID FROM items I WHERE libraryID=?" + idSQL + " AND itemID NOT IN " + + "(SELECT itemID FROM itemAttachments UNION SELECT itemID FROM itemNotes)"; + yield Zotero.DB.queryAsync( + sql, + params, + { + noCache: ids.length != 1, + onRow: function (row) { + var itemID = row.getResultByIndex(0); + var item = this._objectCache[itemID]; + if (!item) { + throw new Error("Item " + itemID + " not loaded"); + } + item._loaded.childItems = true; + item._clearChanged('childItems'); + }.bind(this) + } + ); + }); + + + this._loadTags = Zotero.Promise.coroutine(function* (libraryID, ids, idSQL) { + var sql = "SELECT itemID, name, type FROM items " + + "LEFT JOIN itemTags USING (itemID) " + + "LEFT JOIN tags USING (tagID) WHERE libraryID=?" + idSQL; + var params = [libraryID]; + + var lastItemID; + var rows = []; + var setRows = function (itemID, rows) { + var item = this._objectCache[itemID]; + if (!item) { + throw new Error("Item " + itemID + " not found"); + } - // Display titles - for (let i=0; i<allItemIDs.length; i++) { - let itemID = allItemIDs[i]; - let item = this._objectCache[itemID]; - yield item.loadDisplayTitle() + item._tags = []; + for (let i = 0; i < rows.length; i++) { + let row = rows[i]; + item._tags.push(Zotero.Tags.cleanData(row)); + } + + item._loaded.tags = true; + item._clearChanged('tags'); + }.bind(this); + + yield Zotero.DB.queryAsync( + sql, + params, + { + noCache: ids.length != 1, + onRow: function (row) { + let itemID = row.getResultByIndex(0); + + if (lastItemID && itemID !== lastItemID) { + setRows(lastItemID, rows); + rows = []; + } + + lastItemID = itemID; + + // Item has no tags + let tag = row.getResultByIndex(1); + if (tag === null) { + return; + } + + rows.push({ + tag: tag, + type: row.getResultByIndex(2) + }); + }.bind(this) } + ); + if (lastItemID) { + setRows(lastItemID, rows); } + }); + + + this._loadCollections = Zotero.Promise.coroutine(function* (libraryID, ids, idSQL) { + var sql = "SELECT itemID, collectionID FROM items " + + "LEFT JOIN collectionItems USING (itemID) " + + "WHERE libraryID=?" + idSQL; + var params = [libraryID]; - Zotero.debug("Cached fields in " + ((new Date) - t) + "ms"); + var lastItemID; + var rows = []; + var setRows = function (itemID, rows) { + var item = this._objectCache[itemID]; + if (!item) { + throw new Error("Item " + itemID + " not found"); + } + + item._collections = rows; + item._loaded.collections = true; + item._clearChanged('collections'); + }.bind(this); + + yield Zotero.DB.queryAsync( + sql, + params, + { + noCache: ids.length != 1, + onRow: function (row) { + let itemID = row.getResultByIndex(0); + + if (lastItemID && itemID !== lastItemID) { + setRows(lastItemID, rows); + rows = []; + } + + lastItemID = itemID; + let collectionID = row.getResultByIndex(1); + // No collections + if (collectionID === null) { + return; + } + rows.push(collectionID); + }.bind(this) + } + ); + if (lastItemID) { + setRows(lastItemID, rows); + } }); @@ -409,17 +724,11 @@ Zotero.Items = function() { var otherItemIDs = []; var itemURI = Zotero.URI.getItemURI(item); - yield item.loadTags(); - yield item.loadRelations(); var replPred = Zotero.Relations.replacedItemPredicate; var toSave = {}; toSave[this.id]; for each(var otherItem in otherItems) { - yield otherItem.loadChildItems(); - yield otherItem.loadCollections(); - yield otherItem.loadTags(); - yield otherItem.loadRelations(); let otherItemURI = Zotero.URI.getItemURI(otherItem); // Move child items to master @@ -632,16 +941,6 @@ Zotero.Items = function() { }); - this._postLoad = function (libraryID, ids) { - if (!ids) { - if (!this._cachedFields[libraryID]) { - this._cachedFields[libraryID] = []; - } - this._cachedFields[libraryID] = this.primaryFields.concat(); - } - } - - /* * Generate SQL to retrieve firstCreator field * diff --git a/chrome/content/zotero/xpcom/data/library.js b/chrome/content/zotero/xpcom/data/library.js @@ -315,6 +315,16 @@ Zotero.Library.prototype._reloadFromDB = Zotero.Promise.coroutine(function* () { this._loadDataFromRow(row); }); +/** + * Load object data in this library + */ +Zotero.Library.prototype.loadAllDataTypes = Zotero.Promise.coroutine(function* () { + yield Zotero.SyncedSettings.loadAll(this.libraryID); + yield Zotero.Collections.loadAll(this.libraryID); + yield Zotero.Searches.loadAll(this.libraryID); + yield Zotero.Items.loadAll(this.libraryID); +}); + Zotero.Library.prototype.isChildObjectAllowed = function(type) { return this._childObjectTypes.indexOf(type) != -1; }; @@ -461,6 +471,8 @@ Zotero.Library.prototype._finalizeSave = Zotero.Promise.coroutine(function* (env yield this._reloadFromDB(); Zotero.Libraries.register(this); + + yield this.loadAllDataTypes(); } }); diff --git a/chrome/content/zotero/xpcom/data/tags.js b/chrome/content/zotero/xpcom/data/tags.js @@ -77,7 +77,7 @@ Zotero.Tags = new function() { /** - * Get all tags indexed by tagID + * Get all tags in library * * @param {Number} libraryID * @param {Array} [types] Tag types to fetch @@ -181,7 +181,7 @@ Zotero.Tags = new function() { // We need to know if the old tag has a color assigned so that // we can assign it to the new name - var oldColorData = yield this.getColor(libraryID, oldName); + var oldColorData = this.getColor(libraryID, oldName); yield Zotero.DB.executeTransaction(function* () { var oldItemIDs = yield this.getTagItems(libraryID, oldTagID); @@ -393,13 +393,13 @@ Zotero.Tags = new function() { * * @param {Integer} libraryID * @param {String} name Tag name - * @return {Promise} A Q promise for the tag color as a hex string (e.g., '#990000') + * @return {Object|false} An object containing 'color' as a hex string (e.g., '#990000') and + * 'position', or false if no colored tag with that name */ this.getColor = function (libraryID, name) { - return this.getColors(libraryID) - .then(function () { - return _libraryColorsByName[libraryID].get(name) || false; - }); + // Cache colors + this.getColors(libraryID); + return _libraryColorsByName[libraryID].get(name) || false; } @@ -408,14 +408,12 @@ Zotero.Tags = new function() { * * @param {Integer} libraryID * @param {Integer} position The position of the tag, starting at 0 - * @return {Promise} A promise for an object containing 'name' and 'color' + * @return {Object|false} An object containing 'name' and 'color', or false if no color at + * the given position */ this.getColorByPosition = function (libraryID, position) { - return this.getColors(libraryID) - .then(function () { - return _libraryColors[libraryID][position] - ? _libraryColors[libraryID][position] : false; - }); + this.getColors(libraryID); + return _libraryColors[libraryID][position] ? _libraryColors[libraryID][position] : false; } @@ -423,21 +421,20 @@ Zotero.Tags = new function() { * Get colored tags within a given library * * @param {Integer} libraryID - * @return {Promise<Map>} - A promise for a Map with tag names as keys and - * objects containing 'color' and 'position' as values + * @return {Map} - A Map with tag names as keys and objects containing 'color' and 'position' + * as values */ - this.getColors = Zotero.Promise.coroutine(function* (libraryID) { - if (_libraryColorsByName[libraryID]) { - return _libraryColorsByName[libraryID]; + this.getColors = function (libraryID) { + if (!libraryID) { + throw new Error("libraryID not provided"); } - var tagColors = yield Zotero.SyncedSettings.get(libraryID, 'tagColors'); - - // If the colors became available from another run if (_libraryColorsByName[libraryID]) { return _libraryColorsByName[libraryID]; } + var tagColors = Zotero.SyncedSettings.get(libraryID, 'tagColors'); + tagColors = tagColors || []; _libraryColors[libraryID] = tagColors; @@ -452,7 +449,7 @@ Zotero.Tags = new function() { } return _libraryColorsByName[libraryID]; - }); + }; /** @@ -465,7 +462,7 @@ Zotero.Tags = new function() { throw new Error("libraryID must be an integer"); } - yield this.getColors(libraryID); + this.getColors(libraryID); var tagColors = _libraryColors[libraryID]; @@ -541,7 +538,7 @@ Zotero.Tags = new function() { delete _libraryColorsByName[libraryID]; // Get the tag colors for each library in which they were modified - let tagColors = yield Zotero.SyncedSettings.get(libraryID, 'tagColors'); + let tagColors = Zotero.SyncedSettings.get(libraryID, 'tagColors'); if (!tagColors) { tagColors = []; } diff --git a/chrome/content/zotero/xpcom/date.js b/chrome/content/zotero/xpcom/date.js @@ -87,6 +87,10 @@ Zotero.Date = new function(){ **/ function sqlToDate(sqldate, isUTC){ try { + if (!this.isSQLDate(sqldate) && !this.isSQLDateTime(sqldate)) { + throw new Error("Invalid date"); + } + var datetime = sqldate.split(' '); var dateparts = datetime[0].split('-'); if (datetime[1]){ @@ -98,7 +102,7 @@ Zotero.Date = new function(){ // Invalid date part if (dateparts.length==1){ - return false; + throw new Error("Invalid date part"); } if (isUTC){ @@ -699,7 +703,7 @@ Zotero.Date = new function(){ function toUnixTimestamp(date) { if (date === null || typeof date != 'object' || date.constructor.name != 'Date') { - throw ('Not a valid date in Zotero.Date.toUnixTimestamp()'); + throw new Error(`'${date}' is not a valid date`); } return Math.round(date.getTime() / 1000); } diff --git a/chrome/content/zotero/xpcom/db.js b/chrome/content/zotero/xpcom/db.js @@ -630,7 +630,13 @@ Zotero.DBConnection.prototype.queryAsync = Zotero.Promise.coroutine(function* (s } } } - let rows = yield conn.executeCached(sql, params, onRow); + let rows; + if (options && options.noCache) { + rows = yield conn.execute(sql, params, onRow); + } + else { + rows = yield conn.executeCached(sql, params, onRow); + } // Parse out the SQL command being used let op = sql.match(/^[^a-z]*[^ ]+/i); if (op) { diff --git a/chrome/content/zotero/xpcom/duplicates.js b/chrome/content/zotero/xpcom/duplicates.js @@ -303,13 +303,13 @@ Zotero.Duplicates.prototype._findDuplicates = Zotero.Promise.coroutine(function* itemCreators = []; } } - else { - itemCreators.push({ - lastName: normalizeString(row.lastName), - firstInitial: row.fieldMode == 0 ? normalizeString(row.firstName).charAt(0) : false - }); - } + lastItemID = row.itemID; + + itemCreators.push({ + lastName: normalizeString(row.lastName), + firstInitial: row.fieldMode == 0 ? normalizeString(row.firstName).charAt(0) : false + }); } // Add final item creators if (itemCreators.length) { diff --git a/chrome/content/zotero/xpcom/itemTreeView.js b/chrome/content/zotero/xpcom/itemTreeView.js @@ -82,7 +82,7 @@ Zotero.ItemTreeView.prototype.setTree = Zotero.Promise.coroutine(function* (tree if (this._treebox) { if (this._needsSort) { - yield this.sort(); + this.sort(); } return; } @@ -133,11 +133,11 @@ Zotero.ItemTreeView.prototype.setTree = Zotero.Promise.coroutine(function* (tree if (self._treebox.view.selection.count > 1) { switch (event.keyCode) { case 39: - self.expandSelectedRows().done(); + self.expandSelectedRows(); break; case 37: - self.collapseSelectedRows().done(); + self.collapseSelectedRows(); break; } @@ -148,7 +148,7 @@ Zotero.ItemTreeView.prototype.setTree = Zotero.Promise.coroutine(function* (tree var key = String.fromCharCode(event.which); if (key == '+' && !(event.ctrlKey || event.altKey || event.metaKey)) { - self.expandAllRows().done(); + self.expandAllRows(); event.preventDefault(); return; } @@ -170,12 +170,11 @@ Zotero.ItemTreeView.prototype.setTree = Zotero.Promise.coroutine(function* (tree if (coloredTagsRE.test(key)) { let libraryID = self.collectionTreeRow.ref.libraryID; let position = parseInt(key) - 1; - let colorData = yield Zotero.Tags.getColorByPosition(libraryID, position); + let colorData = Zotero.Tags.getColorByPosition(libraryID, position); // If a color isn't assigned to this number or any // other numbers, allow key navigation if (!colorData) { - let colors = yield Zotero.Tags.getColors(libraryID); - return !colors.size; + return !Zotero.Tags.getColors(libraryID).size; } var items = self.getSelectedItems(); @@ -230,8 +229,8 @@ Zotero.ItemTreeView.prototype.setTree = Zotero.Promise.coroutine(function* (tree // handleKeyPress() in zoteroPane.js. tree._handleEnter = function () {}; - yield this.sort(); - yield this.expandMatchParents(); + this.sort(); + this.expandMatchParents(); if (this._ownerDocument.defaultView.ZoteroPane_Local) { this._ownerDocument.defaultView.ZoteroPane_Local.clearItemsPaneMessage(); @@ -266,13 +265,10 @@ Zotero.ItemTreeView.prototype.setTree = Zotero.Promise.coroutine(function* (tree */ Zotero.ItemTreeView.prototype.refresh = Zotero.serial(Zotero.Promise.coroutine(function* () { Zotero.debug('Refreshing items list for ' + this.id); - //if(!Zotero.ItemTreeView._haveCachedFields) yield Zotero.Promise.resolve(); - var cacheFields = ['title', 'date']; - - // Cache the visible fields so they don't load individually + // DEBUG: necessary? try { - var visibleFields = this.getVisibleFields(); + this._treebox.columns.count } // If treebox isn't ready, skip refresh catch (e) { @@ -286,33 +282,6 @@ Zotero.ItemTreeView.prototype.refresh = Zotero.serial(Zotero.Promise.coroutine(f }); try { - for (let i=0; i<visibleFields.length; i++) { - let field = visibleFields[i]; - switch (field) { - case 'hasAttachment': - // Needed by item.getBestAttachments(), called by getBestAttachmentStateAsync() - field = 'url'; - break; - - case 'numNotes': - continue; - - case 'year': - field = 'date'; - break; - - case 'itemType': - field = 'itemTypeID'; - break; - } - if (cacheFields.indexOf(field) == -1) { - cacheFields = cacheFields.concat(field); - } - } - - yield Zotero.Items.cacheFields(this.collectionTreeRow.ref.libraryID, cacheFields); - Zotero.ItemTreeView._haveCachedFields = true; - Zotero.CollectionTreeCache.clear(); if (!this.selection.selectEventsSuppressed) { @@ -382,9 +351,9 @@ Zotero.ItemTreeView.prototype.refresh = Zotero.serial(Zotero.Promise.coroutine(f this._searchParentIDs = newSearchParentIDs; this._cellTextCache = {}; - yield this.rememberOpenState(savedOpenState); - yield this.rememberSelection(savedSelection); - yield this.expandMatchParents(); + this.rememberOpenState(savedOpenState); + this.rememberSelection(savedSelection); + this.expandMatchParents(); if (unsuppress) { // This causes a problem with the row count being wrong between views //this._treebox.endUpdateBatch(); @@ -404,12 +373,6 @@ Zotero.ItemTreeView.prototype.refresh = Zotero.serial(Zotero.Promise.coroutine(f })); -/** - * Generator used internally for refresh - */ -Zotero.ItemTreeView._haveCachedFields = false; - - /* * Called by Zotero.Notifier on any changes to items in the data layer */ @@ -502,7 +465,7 @@ Zotero.ItemTreeView.prototype.notify = Zotero.Promise.coroutine(function* (actio delete this._cellTextCache[row]; this.selection.clearSelection(); - yield this.rememberSelection(savedSelection); + this.rememberSelection(savedSelection); } else { this._cellTextCache = {}; @@ -569,7 +532,6 @@ Zotero.ItemTreeView.prototype.notify = Zotero.Promise.coroutine(function* (actio push = true; } else { - yield collectionTreeRow.ref.loadChildItems(); push = !collectionTreeRow.ref.hasItem(ids[i]); } // Row might already be gone (e.g. if this is a child and @@ -627,7 +589,7 @@ Zotero.ItemTreeView.prototype.notify = Zotero.Promise.coroutine(function* (actio // If no quicksearch, process modifications manually else if (!quicksearch || quicksearch.value == '') { - var items = yield Zotero.Items.getAsync(ids); + var items = Zotero.Items.get(ids); for (let i = 0; i < items.length; i++) { let item = items[i]; @@ -646,9 +608,16 @@ Zotero.ItemTreeView.prototype.notify = Zotero.Promise.coroutine(function* (actio let parentItemID = this.getRow(row).ref.parentItemID; let parentIndex = this.getParentIndex(row); - // Top-level items just have to be resorted + // Top-level item if (this.isContainer(row)) { - sort = id; + // If Unfiled Items and itm was added to a collection, remove from view + if (collectionTreeRow.isUnfiled() && item.getCollections().length) { + this._removeRow(row); + } + // Otherwise just resort + else { + sort = id; + } } // If item moved from top-level to under another item, remove the old row. else if (parentIndex == -1 && parentItemID) { @@ -679,7 +648,6 @@ Zotero.ItemTreeView.prototype.notify = Zotero.Promise.coroutine(function* (actio && collectionTreeRow.ref.libraryID == item.libraryID; // Collection containing item if (!add && collectionTreeRow.isCollection()) { - yield item.loadCollections(); add = item.inCollection(collectionTreeRow.ref.id); } if (add) { @@ -719,14 +687,7 @@ Zotero.ItemTreeView.prototype.notify = Zotero.Promise.coroutine(function* (actio } else if(action == 'add') { - // New items need their item data and collections loaded - // before they're inserted into the tree let items = yield Zotero.Items.getAsync(ids); - for (let i=0; i<items.length; i++) { - let item = items[i]; - yield item.loadItemData(); - yield item.loadCollections(); - } // In some modes, just re-run search if (collectionTreeRow.isSearch() || collectionTreeRow.isTrash() || collectionTreeRow.isUnfiled()) { @@ -824,7 +785,7 @@ Zotero.ItemTreeView.prototype.notify = Zotero.Promise.coroutine(function* (actio } if (sort) { - yield this.sort(typeof sort == 'number' ? sort : false); + this.sort(typeof sort == 'number' ? sort : false); } else { this._refreshItemRowMap(); @@ -853,7 +814,7 @@ Zotero.ItemTreeView.prototype.notify = Zotero.Promise.coroutine(function* (actio yield this.selectItem(ids[0]); } else { - yield this.rememberSelection(savedSelection); + this.rememberSelection(savedSelection); } } // On removal of a row, select item at previous position @@ -891,7 +852,7 @@ Zotero.ItemTreeView.prototype.notify = Zotero.Promise.coroutine(function* (actio } } else { - yield this.rememberSelection(savedSelection); + this.rememberSelection(savedSelection); } } @@ -1159,8 +1120,7 @@ Zotero.ItemTreeView.prototype.hasNextSibling = function(row,afterIndex) } } -Zotero.ItemTreeView.prototype.toggleOpenState = Zotero.Promise.coroutine(function* (row, skipItemMapRefresh) -{ +Zotero.ItemTreeView.prototype.toggleOpenState = function (row, skipItemMapRefresh) { // Shouldn't happen but does if an item is dragged over a closed // container until it opens and then released, since the container // is no longer in the same place when the spring-load closes @@ -1179,7 +1139,6 @@ Zotero.ItemTreeView.prototype.toggleOpenState = Zotero.Promise.coroutine(functio // Open // var item = this.getRow(row).ref; - yield item.loadChildItems(); //Get children var includeTrashed = this.collectionTreeRow.isTrash(); @@ -1198,7 +1157,7 @@ Zotero.ItemTreeView.prototype.toggleOpenState = Zotero.Promise.coroutine(functio } if (newRows) { - newRows = yield Zotero.Items.getAsync(newRows); + newRows = Zotero.Items.get(newRows); for (let i = 0; i < newRows.length; i++) { count++; @@ -1221,7 +1180,7 @@ Zotero.ItemTreeView.prototype.toggleOpenState = Zotero.Promise.coroutine(functio Zotero.debug('Refreshing hash map'); this._refreshItemRowMap(); } -}); +} Zotero.ItemTreeView.prototype._closeContainer = function (row, skipItemMapRefresh) { @@ -1263,8 +1222,7 @@ Zotero.ItemTreeView.prototype.isSorted = function() return true; } -Zotero.ItemTreeView.prototype.cycleHeader = Zotero.Promise.coroutine(function* (column) -{ +Zotero.ItemTreeView.prototype.cycleHeader = function (column) { for(var i=0, len=this._treebox.columns.count; i<len; i++) { col = this._treebox.columns.getColumnAt(i); @@ -1291,8 +1249,8 @@ Zotero.ItemTreeView.prototype.cycleHeader = Zotero.Promise.coroutine(function* ( if (savedSelection.length == 1) { var pos = this._rowMap[savedSelection[0]] - this._treebox.getFirstVisibleRow(); } - yield this.sort(); - yield this.rememberSelection(savedSelection); + this.sort(); + this.rememberSelection(savedSelection); // If single row was selected, try to keep it in the same place if (savedSelection.length == 1) { var newRow = this._rowMap[savedSelection[0]]; @@ -1304,12 +1262,12 @@ Zotero.ItemTreeView.prototype.cycleHeader = Zotero.Promise.coroutine(function* ( } this._treebox.invalidate(); this.selection.selectEventsSuppressed = false; -}); +} /* * Sort the items by the currently sorted column. */ -Zotero.ItemTreeView.prototype.sort = Zotero.Promise.coroutine(function* (itemID) { +Zotero.ItemTreeView.prototype.sort = function (itemID) { var t = new Date; // If Zotero pane is hidden, mark tree for sorting later in setTree() @@ -1324,7 +1282,7 @@ Zotero.ItemTreeView.prototype.sort = Zotero.Promise.coroutine(function* (itemID) this.getRow(this._rowMap[itemID]).ref.parentKey) { let parentIndex = this.getParentIndex(this._rowMap[itemID]); this._closeContainer(parentIndex); - yield this.toggleOpenState(parentIndex); + this.toggleOpenState(parentIndex); return; } @@ -1566,8 +1524,8 @@ Zotero.ItemTreeView.prototype.sort = Zotero.Promise.coroutine(function* (itemID) this._refreshItemRowMap(); - yield this.rememberOpenState(openItemIDs); - yield this.rememberSelection(savedSelection); + this.rememberOpenState(openItemIDs); + this.rememberSelection(savedSelection); if (unsuppress) { this.selection.selectEventsSuppressed = false; @@ -1575,7 +1533,7 @@ Zotero.ItemTreeView.prototype.sort = Zotero.Promise.coroutine(function* (itemID) } Zotero.debug("Sorted items list in " + (new Date - t) + " ms"); -}); +}; //////////////////////////////////////////////////////////////////////////////// @@ -1633,7 +1591,7 @@ Zotero.ItemTreeView.prototype.selectItem = Zotero.Promise.coroutine(function* (i // Clear the quicksearch and tag selection and try again (once) if (!noRecurse && this._ownerDocument.defaultView.ZoteroPane_Local) { let cleared1 = yield this._ownerDocument.defaultView.ZoteroPane_Local.clearQuicksearch(); - let cleared2 = yield this._ownerDocument.defaultView.ZoteroPane_Local.clearTagSelection(); + let cleared2 = this._ownerDocument.defaultView.ZoteroPane_Local.clearTagSelection(); if (cleared1 || cleared2) { return this.selectItem(id, expand, true); } @@ -1648,7 +1606,7 @@ Zotero.ItemTreeView.prototype.selectItem = Zotero.Promise.coroutine(function* (i this._closeContainer(parentRow); // Open the parent - yield this.toggleOpenState(parentRow); + this.toggleOpenState(parentRow); row = this._rowMap[id]; } @@ -1669,7 +1627,7 @@ Zotero.ItemTreeView.prototype.selectItem = Zotero.Promise.coroutine(function* (i // If |expand|, open row if container if (expand && this.isContainer(row) && !this.isContainerOpen(row)) { - yield this.toggleOpenState(row); + this.toggleOpenState(row); } this.selection.select(row); @@ -1836,7 +1794,7 @@ Zotero.ItemTreeView.prototype.setFilter = Zotero.Promise.coroutine(function* (ty var oldCount = this.rowCount; yield this.refresh(); - yield this.sort(); + this.sort(); //this._treebox.endUpdateBatch(); this.selection.selectEventsSuppressed = false; @@ -1869,8 +1827,7 @@ Zotero.ItemTreeView.prototype.saveSelection = function () { /* * Sets the selection based on saved selection ids */ -Zotero.ItemTreeView.prototype.rememberSelection = Zotero.Promise.coroutine(function* (selection) -{ +Zotero.ItemTreeView.prototype.rememberSelection = function (selection) { if (!selection.length) { return; } @@ -1888,7 +1845,7 @@ Zotero.ItemTreeView.prototype.rememberSelection = Zotero.Promise.coroutine(funct } // Try the parent else { - var item = yield Zotero.Items.getAsync(selection[i]); + var item = Zotero.Items.get(selection[i]); if (!item) { continue; } @@ -1900,7 +1857,7 @@ Zotero.ItemTreeView.prototype.rememberSelection = Zotero.Promise.coroutine(funct if (this._rowMap[parent] != null) { this._closeContainer(this._rowMap[parent]); - yield this.toggleOpenState(this._rowMap[parent]); + this.toggleOpenState(this._rowMap[parent]); this.selection.toggleSelect(this._rowMap[selection[i]]); } } @@ -1909,21 +1866,21 @@ Zotero.ItemTreeView.prototype.rememberSelection = Zotero.Promise.coroutine(funct //this._treebox.endUpdateBatch(); this.selection.selectEventsSuppressed = false; } -}); +} -Zotero.ItemTreeView.prototype.selectSearchMatches = Zotero.Promise.coroutine(function* () { +Zotero.ItemTreeView.prototype.selectSearchMatches = function () { if (this._searchMode) { var ids = []; for (var id in this._searchItemIDs) { ids.push(id); } - yield this.rememberSelection(ids); + this.rememberSelection(ids); } else { this.selection.clearSelection(); } -}); +} Zotero.ItemTreeView.prototype._saveOpenState = function (close) { @@ -1953,7 +1910,7 @@ Zotero.ItemTreeView.prototype._saveOpenState = function (close) { } -Zotero.ItemTreeView.prototype.rememberOpenState = Zotero.Promise.coroutine(function* (itemIDs) { +Zotero.ItemTreeView.prototype.rememberOpenState = function (itemIDs) { var rowsToOpen = []; for each(var id in itemIDs) { var row = this._rowMap[id]; @@ -1973,17 +1930,17 @@ Zotero.ItemTreeView.prototype.rememberOpenState = Zotero.Promise.coroutine(funct } // Reopen from bottom up for (var i=rowsToOpen.length-1; i>=0; i--) { - yield this.toggleOpenState(rowsToOpen[i], true); + this.toggleOpenState(rowsToOpen[i], true); } this._refreshItemRowMap(); if (unsuppress) { //this._treebox.endUpdateBatch(); this.selection.selectEventsSuppressed = false; } -}); +} -Zotero.ItemTreeView.prototype.expandMatchParents = Zotero.Promise.coroutine(function* () { +Zotero.ItemTreeView.prototype.expandMatchParents = function () { // Expand parents of child matches if (!this._searchMode) { return; @@ -2001,7 +1958,7 @@ Zotero.ItemTreeView.prototype.expandMatchParents = Zotero.Promise.coroutine(func for (var i=0; i<this.rowCount; i++) { var id = this.getRow(i).ref.id; if (hash[id] && this.isContainer(i) && !this.isContainerOpen(i)) { - yield this.toggleOpenState(i, true); + this.toggleOpenState(i, true); } } this._refreshItemRowMap(); @@ -2009,7 +1966,7 @@ Zotero.ItemTreeView.prototype.expandMatchParents = Zotero.Promise.coroutine(func //this._treebox.endUpdateBatch(); this.selection.selectEventsSuppressed = false; } -}); +} Zotero.ItemTreeView.prototype.saveFirstRow = function() { @@ -2028,18 +1985,18 @@ Zotero.ItemTreeView.prototype.rememberFirstRow = function(firstRow) { } -Zotero.ItemTreeView.prototype.expandAllRows = Zotero.Promise.coroutine(function* () { +Zotero.ItemTreeView.prototype.expandAllRows = function () { var unsuppress = this.selection.selectEventsSuppressed = true; //this._treebox.beginUpdateBatch(); for (var i=0; i<this.rowCount; i++) { if (this.isContainer(i) && !this.isContainerOpen(i)) { - yield this.toggleOpenState(i, true); + this.toggleOpenState(i, true); } } this._refreshItemRowMap(); //this._treebox.endUpdateBatch(); this.selection.selectEventsSuppressed = false; -}); +} Zotero.ItemTreeView.prototype.collapseAllRows = Zotero.Promise.coroutine(function* () { @@ -2056,7 +2013,7 @@ Zotero.ItemTreeView.prototype.collapseAllRows = Zotero.Promise.coroutine(functio }); -Zotero.ItemTreeView.prototype.expandSelectedRows = Zotero.Promise.coroutine(function* () { +Zotero.ItemTreeView.prototype.expandSelectedRows = function () { var start = {}, end = {}; this.selection.selectEventsSuppressed = true; //this._treebox.beginUpdateBatch(); @@ -2064,17 +2021,17 @@ Zotero.ItemTreeView.prototype.expandSelectedRows = Zotero.Promise.coroutine(func this.selection.getRangeAt(i, start, end); for (var j = start.value; j <= end.value; j++) { if (this.isContainer(j) && !this.isContainerOpen(j)) { - yield this.toggleOpenState(j, true); + this.toggleOpenState(j, true); } } } this._refreshItemRowMap(); //this._treebox.endUpdateBatch(); this.selection.selectEventsSuppressed = false; -}); +} -Zotero.ItemTreeView.prototype.collapseSelectedRows = Zotero.Promise.coroutine(function* () { +Zotero.ItemTreeView.prototype.collapseSelectedRows = function () { var start = {}, end = {}; this.selection.selectEventsSuppressed = true; //this._treebox.beginUpdateBatch(); @@ -2089,7 +2046,7 @@ Zotero.ItemTreeView.prototype.collapseSelectedRows = Zotero.Promise.coroutine(fu this._refreshItemRowMap(); //this._treebox.endUpdateBatch(); this.selection.selectEventsSuppressed = false; -}); +} Zotero.ItemTreeView.prototype.getVisibleFields = function() { @@ -2392,17 +2349,16 @@ Zotero.ItemTreeCommandController.prototype.isCommandEnabled = function(cmd) return (cmd == 'cmd_selectAll'); } -Zotero.ItemTreeCommandController.prototype.doCommand = Zotero.Promise.coroutine(function* (cmd) -{ +Zotero.ItemTreeCommandController.prototype.doCommand = function (cmd) { if (cmd == 'cmd_selectAll') { if (this.tree.view.wrappedJSObject.collectionTreeRow.isSearchMode()) { - yield this.tree.view.wrappedJSObject.selectSearchMatches(); + this.tree.view.wrappedJSObject.selectSearchMatches(); } else { this.tree.view.selection.selectAll(); } } -}); +} Zotero.ItemTreeCommandController.prototype.onEvent = function(evt) { @@ -2474,10 +2430,6 @@ Zotero.ItemTreeView.prototype.onDragStart = function (event) { } } - // TEMP - Zotero.debug("TEMP: Skipping Quick Copy"); - return; - // Get Quick Copy format for current URL var url = this._ownerDocument.defaultView.content && this._ownerDocument.defaultView.content.location ? this._ownerDocument.defaultView.content.location.href : null; @@ -2937,7 +2889,6 @@ Zotero.ItemTreeView.prototype.drop = Zotero.Promise.coroutine(function* (row, or for (let i=0; i<items.length; i++) { let item = items[i]; var source = item.isRegularItem() ? false : item.parentItemID; - yield item.loadCollections(); // Top-level item if (source) { item.parentID = false; diff --git a/chrome/content/zotero/xpcom/libraryTreeView.js b/chrome/content/zotero/xpcom/libraryTreeView.js @@ -79,7 +79,7 @@ Zotero.LibraryTreeView.prototype = { * Return the index of the row with a given ID (e.g., "C123" for collection 123) * * @param {String} - Row id - * @return {Integer} + * @return {Integer|false} */ getRowIndexByID: function (id) { var type = ""; @@ -87,7 +87,7 @@ Zotero.LibraryTreeView.prototype = { var type = id[0]; id = ('' + id).substr(1); } - return this._rowMap[type + id]; + return this._rowMap[type + id] !== undefined ? this._rowMap[type + id] : false; }, diff --git a/chrome/content/zotero/xpcom/schema.js b/chrome/content/zotero/xpcom/schema.js @@ -542,7 +542,7 @@ Zotero.Schema = new function(){ var Mode = Zotero.Utilities.capitalize(mode); var repotime = yield Zotero.File.getContentsFromURLAsync("resource://zotero/schema/repotime.txt"); - var date = Zotero.Date.sqlToDate(repotime, true); + var date = Zotero.Date.sqlToDate(repotime.trim(), true); repotime = Zotero.Date.toUnixTimestamp(date); var fileNameRE = new RegExp("^[^\.].+\\" + fileExt + "$"); diff --git a/chrome/content/zotero/xpcom/search.js b/chrome/content/zotero/xpcom/search.js @@ -382,6 +382,7 @@ Zotero.Search.prototype.addCondition = function (condition, operator, value, req this._sqlParams = false; this._markFieldChange('conditions', this._conditions); this._changed.conditions = true; + return searchConditionID; } @@ -499,14 +500,11 @@ Zotero.Search.prototype.hasPostSearchFilter = function() { Zotero.Search.prototype.search = Zotero.Promise.coroutine(function* (asTempTable) { var tmpTable; - if (this._identified) { - yield this.loadConditions(); - } // Mark conditions as loaded - else { + // TODO: Necessary? + if (!this._identified) { this._requireData('conditions'); } - try { if (!this._sql){ yield this._buildQuery(); @@ -554,11 +552,6 @@ Zotero.Search.prototype.search = Zotero.Promise.coroutine(function* (asTempTable // Run a subsearch to define the superset of possible results if (this._scope) { - if (this._scope._identified) { - yield this._scope.loadPrimaryData(); - yield this._scope.loadConditions(); - } - // If subsearch has post-search filter, run and insert ids into temp table if (this._scope.hasPostSearchFilter()) { var ids = yield this._scope.search(); @@ -822,7 +815,7 @@ Zotero.Search.prototype.search = Zotero.Promise.coroutine(function* (asTempTable /** * Populate the object's data from an API JSON data object * - * If this object is identified (has an id or library/key), loadAllData() must have been called. + * If this object is identified (has an id or library/key), loadAll() must have been called. */ Zotero.Search.prototype.fromJSON = function (json) { if (!json.name) { @@ -840,13 +833,13 @@ Zotero.Search.prototype.fromJSON = function (json) { } } -Zotero.Collection.prototype.toResponseJSON = Zotero.Promise.coroutine(function* (options = {}) { - var json = yield this.constructor._super.prototype.toResponseJSON.apply(this, options); +Zotero.Collection.prototype.toResponseJSON = function (options = {}) { + var json = this.constructor._super.prototype.toResponseJSON.apply(this, options); return json; -}); +}; -Zotero.Search.prototype.toJSON = Zotero.Promise.coroutine(function* (options = {}) { +Zotero.Search.prototype.toJSON = function (options = {}) { var env = this._preToJSON(options); var mode = env.mode; @@ -854,11 +847,10 @@ Zotero.Search.prototype.toJSON = Zotero.Promise.coroutine(function* (options = { obj.key = this.key; obj.version = this.version; obj.name = this.name; - yield this.loadConditions(); obj.conditions = this.getConditions(); return this._postToJSON(env); -}); +} /* @@ -866,7 +858,6 @@ Zotero.Search.prototype.toJSON = Zotero.Promise.coroutine(function* (options = { */ Zotero.Search.prototype.getSQL = Zotero.Promise.coroutine(function* () { if (!this._sql) { - yield this.loadConditions(); yield this._buildQuery(); } return this._sql; @@ -875,68 +866,12 @@ Zotero.Search.prototype.getSQL = Zotero.Promise.coroutine(function* () { Zotero.Search.prototype.getSQLParams = Zotero.Promise.coroutine(function* () { if (!this._sql) { - yield this.loadConditions(); yield this._buildQuery(); } return this._sqlParams; }); -Zotero.Search.prototype.loadConditions = Zotero.Promise.coroutine(function* (reload) { - if (this._loaded.conditions && !reload) return; - - Zotero.debug("Loading conditions for search " + this.libraryKey); - - if (!this.id) { - throw new Error('ID not set for object before attempting to load conditions'); - } - - var sql = "SELECT * FROM savedSearchConditions " - + "WHERE savedSearchID=? ORDER BY searchConditionID"; - var conditions = yield Zotero.DB.queryAsync(sql, this.id); - - if (conditions.length) { - this._maxSearchConditionID = conditions[conditions.length - 1].searchConditionID; - } - - this._conditions = {}; - - // Reindex conditions, in case they're not contiguous in the DB - for (let i=0; i<conditions.length; i++) { - let condition = conditions[i]; - - // Parse "condition[/mode]" - let [conditionName, mode] = Zotero.SearchConditions.parseCondition(condition.condition); - - let cond = Zotero.SearchConditions.get(conditionName); - if (!cond || cond.noLoad) { - Zotero.debug("Invalid saved search condition '" + conditionName + "' -- skipping", 2); - continue; - } - - // Convert itemTypeID to itemType - // - // TEMP: This can be removed at some point - if (conditionName == 'itemTypeID') { - conditionName = 'itemType'; - condition.value = Zotero.ItemTypes.getName(condition.value); - } - - this._conditions[i] = { - id: i, - condition: conditionName, - mode: mode, - operator: condition.operator, - value: condition.value, - required: !!condition.required - }; - } - - this._loaded.conditions = true; - this._clearChanged('conditions'); -}); - - /* * Batch insert */ @@ -1687,26 +1622,18 @@ Zotero.Searches = function() { * @param {Integer} [libraryID] */ this.getAll = Zotero.Promise.coroutine(function* (libraryID) { - var sql = "SELECT savedSearchID AS id, savedSearchName AS name FROM savedSearches "; - if (libraryID) { - sql += "WHERE libraryID=? "; - var params = libraryID; + var sql = "SELECT savedSearchID FROM savedSearches WHERE libraryID=?"; + var ids = yield Zotero.DB.columnQueryAsync(sql, libraryID); + if (!ids.length) { + return [] } - var rows = yield Zotero.DB.queryAsync(sql, params); + var searches = this.get(ids); // Do proper collation sort var collation = Zotero.getLocaleCollation(); - rows.sort(function (a, b) { + searches.sort(function (a, b) { return collation.compareString(1, a.name, b.name); }); - - var searches = []; - for (var i=0; i<rows.length; i++) { - let search = new Zotero.Search; - search.id = rows[i].id; - yield search.loadPrimaryData(); - searches.push(search); - } return searches; }); @@ -1719,6 +1646,95 @@ Zotero.Searches = function() { + "FROM savedSearches O WHERE 1"; } + + this._loadConditions = Zotero.Promise.coroutine(function* (libraryID, ids, idSQL) { + var sql = "SELECT savedSearchID, searchConditionID, condition, operator, value, required " + + "FROM savedSearches LEFT JOIN savedSearchConditions USING (savedSearchID) " + + "WHERE libraryID=?" + idSQL + + "ORDER BY savedSearchID, searchConditionID"; + var params = [libraryID]; + var lastID = null; + var rows = []; + var setRows = function (searchID, rows) { + var search = this._objectCache[searchID]; + if (!search) { + throw new Error("Search " + searchID + " not found"); + } + + search._conditions = {}; + + if (rows.length) { + search._maxSearchConditionID = rows[rows.length - 1].searchConditionID; + } + + // Reindex conditions, in case they're not contiguous in the DB + for (let i = 0; i < rows.length; i++) { + let condition = rows[i]; + + // Parse "condition[/mode]" + let [conditionName, mode] = Zotero.SearchConditions.parseCondition(condition.condition); + + let cond = Zotero.SearchConditions.get(conditionName); + if (!cond || cond.noLoad) { + Zotero.debug("Invalid saved search condition '" + conditionName + "' -- skipping", 2); + continue; + } + + // Convert itemTypeID to itemType + // + // TEMP: This can be removed at some point + if (conditionName == 'itemTypeID') { + conditionName = 'itemType'; + condition.value = Zotero.ItemTypes.getName(condition.value); + } + + search._conditions[i] = { + id: i, + condition: conditionName, + mode: mode, + operator: condition.operator, + value: condition.value, + required: !!condition.required + }; + } + search._loaded.conditions = true; + search._clearChanged('conditions'); + }.bind(this); + + yield Zotero.DB.queryAsync( + sql, + params, + { + noCache: ids.length != 1, + onRow: function (row) { + let searchID = row.getResultByIndex(0); + + if (lastID && searchID != lastID) { + setRows(lastID, rows); + rows = []; + } + + lastID = searchID; + let searchConditionID = row.getResultByIndex(1); + // No conditions + if (searchConditionID === null) { + return; + } + rows.push({ + searchConditionID, + condition: row.getResultByIndex(2), + operator: row.getResultByIndex(3), + value: row.getResultByIndex(4), + required: row.getResultByIndex(5) + }); + }.bind(this) + } + ); + if (lastID) { + setRows(lastID, rows); + } + }); + Zotero.DataObjects.call(this); return this; diff --git a/chrome/content/zotero/xpcom/storage.js b/chrome/content/zotero/xpcom/storage.js @@ -65,9 +65,9 @@ Zotero.Sync.Storage = new function () { return false; } - var syncModTime = Zotero.Sync.Storage.getSyncedModificationTime(itemID); + var syncModTime = item.attachmentSyncedModificationTime; if (fileModTime != syncModTime) { - var syncHash = Zotero.Sync.Storage.getSyncedHash(itemID); + var syncHash = item.attachmentSyncedHash; if (syncHash) { var fileHash = item.attachmentHash; if (fileHash && fileHash == syncHash) { diff --git a/chrome/content/zotero/xpcom/storage/storageEngine.js b/chrome/content/zotero/xpcom/storage/storageEngine.js @@ -273,7 +273,7 @@ Zotero.Sync.Storage.Engine.prototype.stop = function () { } Zotero.Sync.Storage.Engine.prototype.queueItem = Zotero.Promise.coroutine(function* (item) { - switch (yield this.local.getSyncState(item.id)) { + switch (item.attachmentSyncState) { case Zotero.Sync.Storage.Local.SYNC_STATE_TO_DOWNLOAD: case Zotero.Sync.Storage.Local.SYNC_STATE_FORCE_DOWNLOAD: var type = 'download'; @@ -295,7 +295,7 @@ Zotero.Sync.Storage.Engine.prototype.queueItem = Zotero.Promise.coroutine(functi return; default: - throw new Error("Invalid sync state " + (yield this.local.getSyncState(item.id))); + throw new Error("Invalid sync state " + item.attachmentSyncState); } var request = new Zotero.Sync.Storage.Request({ diff --git a/chrome/content/zotero/xpcom/storage/storageLocal.js b/chrome/content/zotero/xpcom/storage/storageLocal.js @@ -239,7 +239,8 @@ Zotero.Sync.Storage.Local = { // TODO: Catch error? let state = yield this._checkForUpdatedFile(item, attachmentData[item.id]); if (state !== false) { - yield Zotero.Sync.Storage.Local.setSyncState(item.id, state); + item.attachmentSyncState = state; + yield item.saveTx({ skipAll: true }); changed = true; } } @@ -282,7 +283,7 @@ Zotero.Sync.Storage.Local = { // If file is already marked for upload, skip check. Even if the file was changed // both locally and remotely, conflicts are checked at upload time, so we don't need // to worry about it here. - if ((yield this.getSyncState(item.id)) == this.SYNC_STATE_TO_UPLOAD) { + if (item.attachmentSyncState == this.SYNC_STATE_TO_UPLOAD) { Zotero.debug("File is already marked for upload"); return false; } @@ -298,7 +299,7 @@ Zotero.Sync.Storage.Local = { Zotero.debug(`Remote mod time for item ${lk} is ${remoteModTime}`); // Ignore attachments whose stored mod times haven't changed - mtime = mtime !== false ? mtime : (yield this.getSyncedModificationTime(item.id)); + mtime = mtime !== false ? mtime : item.attachmentSyncedModificationTime; if (mtime == remoteModTime) { Zotero.debug(`Synced mod time (${mtime}) hasn't changed for item ${lk}`); return false; @@ -474,125 +475,23 @@ Zotero.Sync.Storage.Local = { }, - /** - * @param {Integer} itemID - */ - getSyncState: function (itemID) { - var sql = "SELECT syncState FROM itemAttachments WHERE itemID=?"; - return Zotero.DB.valueQueryAsync(sql, itemID); - }, - - - /** - * @param {Integer} itemID - * @param {Integer|String} syncState - Zotero.Sync.Storage.Local.SYNC_STATE_* or last part - * as string (e.g., "TO_UPLOAD") - */ - setSyncState: Zotero.Promise.method(function (itemID, syncState) { - if (typeof syncState == 'string') { - syncState = this["SYNC_STATE_" + syncState.toUpperCase()]; - } - - switch (syncState) { - case this.SYNC_STATE_TO_UPLOAD: - case this.SYNC_STATE_TO_DOWNLOAD: - case this.SYNC_STATE_IN_SYNC: - case this.SYNC_STATE_FORCE_UPLOAD: - case this.SYNC_STATE_FORCE_DOWNLOAD: - case this.SYNC_STATE_IN_CONFLICT: - break; - - default: - throw new Error("Invalid sync state " + syncState); - } - - var sql = "UPDATE itemAttachments SET syncState=? WHERE itemID=?"; - return Zotero.DB.valueQueryAsync(sql, [syncState, itemID]); - }), - - - resetModeSyncStates: Zotero.Promise.coroutine(function* (mode) { - var sql = "UPDATE itemAttachments SET syncState=? " - + "WHERE itemID IN (SELECT itemID FROM items WHERE libraryID=?)"; - var params = [this.SYNC_STATE_TO_UPLOAD, Zotero.Libraries.userLibraryID]; - yield Zotero.DB.queryAsync(sql, params); - }), - - - /** - * @param {Integer} itemID - * @return {Integer|NULL} Mod time as timestamp in ms, - * or NULL if never synced - */ - getSyncedModificationTime: Zotero.Promise.coroutine(function* (itemID) { - var sql = "SELECT storageModTime FROM itemAttachments WHERE itemID=?"; - var mtime = yield Zotero.DB.valueQueryAsync(sql, itemID); - if (mtime === false) { - throw new Error("Item " + itemID + " not found") - } - return mtime; - }), - - - /** - * @param {Integer} itemID - * @param {Integer} mtime - File modification time as timestamp in ms - * @param {Boolean} [updateItem=FALSE] - Mark attachment item as unsynced - */ - setSyncedModificationTime: Zotero.Promise.coroutine(function* (itemID, mtime, updateItem) { - mtime = parseInt(mtime) - if (isNaN(mtime) || mtime < 0) { - Components.utils.reportError("Invalid file mod time " + mtime - + " in Zotero.Storage.setSyncedModificationTime()"); - mtime = 0; - } - - Zotero.DB.requireTransaction(); - - var sql = "UPDATE itemAttachments SET storageModTime=? WHERE itemID=?"; - yield Zotero.DB.queryAsync(sql, [mtime, itemID]); - - if (updateItem) { - let item = yield Zotero.Items.getAsync(itemID); - yield item.updateSynced(false); - } - }), - - - /** - * @param {Integer} itemID - * @return {Promise<String|null|false>} - File hash, null if never synced, if false if - * file doesn't exist - */ - getSyncedHash: Zotero.Promise.coroutine(function* (itemID) { - var sql = "SELECT storageHash FROM itemAttachments WHERE itemID=?"; - var hash = yield Zotero.DB.valueQueryAsync(sql, itemID); - if (hash === false) { - throw new Error("Item " + itemID + " not found"); - } - return hash; - }), - - - /** - * @param {Integer} itemID - * @param {String} hash File hash - * @param {Boolean} [updateItem=FALSE] - Mark attachment item as unsynced - */ - setSyncedHash: Zotero.Promise.coroutine(function* (itemID, hash, updateItem) { - if (hash !== null && hash.length != 32) { - throw new Error("Invalid file hash '" + hash + "'"); + resetModeSyncStates: Zotero.Promise.coroutine(function* () { + var sql = "SELECT itemID FROM items JOIN itemAttachments USING (itemID) " + + "WHERE libraryID=? AND itemTypeID=? AND linkMode IN (?, ?)"; + var params = [ + Zotero.Libraries.userLibraryID, + Zotero.ItemTypes.getID('attachment'), + Zotero.Attachments.LINK_MODE_IMPORTED_FILE, + Zotero.Attachments.LINK_MODE_IMPORTED_URL, + ]; + var itemIDs = yield Zotero.DB.columnQueryAsync(sql, params); + for (let itemID of items) { + let item = Zotero.Items.get(itemID); + item._attachmentSyncState = this.SYNC_STATE_TO_UPLOAD; } - Zotero.DB.requireTransaction(); - - var sql = "UPDATE itemAttachments SET storageHash=? WHERE itemID=?"; - yield Zotero.DB.queryAsync(sql, [hash, itemID]); - - if (updateItem) { - let item = yield Zotero.Items.getAsync(itemID); - yield item.updateSynced(false); - } + sql = "UPDATE itemAttachments SET syncState=? WHERE itemID IN (" + sql + ")"; + yield Zotero.DB.queryAsync(sql, [this.SYNC_STATE_TO_UPLOAD].concat(params)); }), @@ -678,11 +577,10 @@ Zotero.Sync.Storage.Local = { // Set the file mtime to the time from the server yield OS.File.setDates(path, null, new Date(parseInt(mtime))); - yield Zotero.DB.executeTransaction(function* () { - yield this.setSyncedHash(item.id, md5); - yield this.setSyncState(item.id, this.SYNC_STATE_IN_SYNC); - yield this.setSyncedModificationTime(item.id, mtime); - }.bind(this)); + item.attachmentSyncedModificationTime = mtime; + item.attachmentSyncedHash = md5; + item.attachmentSyncState = "in_sync"; + yield item.saveTx({ skipAll: true }); return new Zotero.Sync.Storage.Result({ localChanges: true @@ -1040,7 +938,7 @@ Zotero.Sync.Storage.Local = { for (let localItem of localItems) { // Use the mtime for the dateModified field, since that's all that's shown in the // CR window at the moment - let localItemJSON = yield localItem.toJSON(); + let localItemJSON = localItem.toJSON(); localItemJSON.dateModified = Zotero.Date.dateToISO( new Date(yield localItem.attachmentModificationTime) ); @@ -1101,8 +999,9 @@ Zotero.Sync.Storage.Local = { else { syncState = this.SYNC_STATE_FORCE_DOWNLOAD; } - let itemID = Zotero.Items.getIDFromLibraryAndKey(libraryID, conflict.left.key); - yield Zotero.Sync.Storage.Local.setSyncState(itemID, syncState); + let item = Zotero.Items.getByLibraryAndKey(libraryID, conflict.left.key); + item.attachmentSyncState = syncState; + yield item.save({ skipAll: true }); } }.bind(this)); return true; diff --git a/chrome/content/zotero/xpcom/storage/streamListener.js b/chrome/content/zotero/xpcom/storage/streamListener.js @@ -156,6 +156,8 @@ Zotero.Sync.Storage.StreamListener.prototype = { if (!result) { oldChannel.cancel(Components.results.NS_BINDING_ABORTED); newChannel.cancel(Components.results.NS_BINDING_ABORTED); + Zotero.debug("Cancelling redirect"); + // TODO: Prevent onStateChange error return false; } } diff --git a/chrome/content/zotero/xpcom/storage/webdav.js b/chrome/content/zotero/xpcom/storage/webdav.js @@ -288,15 +288,14 @@ Zotero.Sync.Storage.Mode.WebDAV.prototype = { Zotero.debug("File mod time matches remote file -- skipping download of " + item.libraryKey); - yield Zotero.DB.executeTransaction(function* () { - var syncState = Zotero.Sync.Storage.Local.getSyncState(item.id); - // DEBUG: Necessary to update item? - var updateItem = syncState != 1; - yield Zotero.Sync.Storage.Local.setSyncedModificationTime( - item.id, metadata.mtime, updateItem - ); - yield Zotero.Sync.Storage.Local.setSyncState(item.id, "in_sync"); - }); + var updateItem = item.attachmentSyncState != 1 + item.attachmentSyncedModificationTime = metadata.mtime; + item.attachmentSyncState = "in_sync"; + yield item.saveTx({ skipAll: true }); + // DEBUG: Necessary? + if (updateItem) { + yield item.updateSynced(false); + } return new Zotero.Sync.Storage.Result({ localChanges: true, // ? @@ -416,7 +415,7 @@ Zotero.Sync.Storage.Mode.WebDAV.prototype = { } // Check if file already exists on WebDAV server - if ((yield Zotero.Sync.Storage.Local.getSyncState(item.id)) + if (item.attachmentSyncState != Zotero.Sync.Storage.Local.SYNC_STATE_FORCE_UPLOAD) { if (metadata.mtime) { // Local file time @@ -438,15 +437,14 @@ Zotero.Sync.Storage.Mode.WebDAV.prototype = { // If WebDAV server already has file, update synced properties if (!changed) { - yield Zotero.DB.executeTransaction(function* () { - yield Zotero.Sync.Storage.Local.setSyncedModificationTime( - item.id, fmtime, true - ); - if (hash) { - yield Zotero.Sync.Storage.Local.setSyncedHash(item.id, hash); - } - yield Zotero.Sync.Storage.Local.setSyncState(item.id, "in_sync"); - }); + item.attachmentSyncedModificationTime = fmtime; + if (hash) { + item.attachmentSyncedHash = hash; + } + item.attachmentSyncState = "in_sync"; + yield item.saveTx({ skipAll: true }); + // skipAll doesn't mark as unsynced, so do that separately + yield item.updateSynced(false); return new Zotero.Sync.Storage.Result; } } @@ -460,9 +458,9 @@ Zotero.Sync.Storage.Mode.WebDAV.prototype = { // API would ever be updated with the correct values, so we can't just wait for // the API to change.) If a conflict is found, we flag the item as in conflict // and require another file sync, which will trigger conflict resolution. - let smtime = yield Zotero.Sync.Storage.Local.getSyncedModificationTime(item.id); + let smtime = item.attachmentSyncedModificationTime; if (smtime != mtime) { - let shash = yield Zotero.Sync.Storage.Local.getSyncedHash(item.id); + let shash = item.attachmentSyncedHash; if (shash && metadata.md5 && shash == metadata.md5) { Zotero.debug("Last synced mod time for item " + item.libraryKey + " doesn't match time on storage server but hash does -- ignoring"); @@ -472,12 +470,13 @@ Zotero.Sync.Storage.Mode.WebDAV.prototype = { Zotero.logError("Conflict -- last synced file mod time for item " + item.libraryKey + " does not match time on storage server" + " (" + smtime + " != " + mtime + ")"); - yield Zotero.DB.executeTransaction(function* () { - // Conflict resolution uses the synced mtime as the remote value, so set - // that to the WebDAV value, since that's the one in conflict. - yield Zotero.Sync.Storage.Local.setSyncedModificationTime(item.id, mtime); - yield Zotero.Sync.Storage.Local.setSyncState(item.id, "in_conflict"); - }); + + // Conflict resolution uses the synced mtime as the remote value, so set + // that to the WebDAV value, since that's the one in conflict. + item.attachmentSyncedModificationTime = mtime; + item.attachmentSyncState = "in_conflict"; + yield item.saveTx({ skipAll: true }); + return new Zotero.Sync.Storage.Result({ fileSyncRequired: true }); @@ -1073,7 +1072,7 @@ Zotero.Sync.Storage.Mode.WebDAV.prototype = { response, ".//D:getlastmodified", { D: 'DAV:' } ); lastModified = Zotero.Date.strToISO(lastModified); - lastModified = Zotero.Date.sqlToDate(lastModified); + lastModified = Zotero.Date.sqlToDate(lastModified, true); // Delete files older than a day before last sync time var days = (lastSyncDate - lastModified) / 1000 / 60 / 60 / 24; @@ -1191,7 +1190,10 @@ Zotero.Sync.Storage.Mode.WebDAV.prototype = { throw new Error(Zotero.Sync.Storage.Mode.WebDAV.defaultError); } - return { mtime, md5 }; + return { + mtime: parseInt(mtime), + md5 + }; }), @@ -1243,11 +1245,12 @@ Zotero.Sync.Storage.Mode.WebDAV.prototype = { // Update .prop file on WebDAV server yield this._setStorageFileMetadata(item); - yield Zotero.DB.executeTransaction(function* () { - yield Zotero.Sync.Storage.Local.setSyncState(item.id, "in_sync"); - yield Zotero.Sync.Storage.Local.setSyncedModificationTime(item.id, params.mtime, true); - yield Zotero.Sync.Storage.Local.setSyncedHash(item.id, params.md5); - }); + item.attachmentSyncedModificationTime = params.mtime; + item.attachmentSyncedHash = params.md5; + item.attachmentSyncState = "in_sync"; + yield item.saveTx({ skipAll: true }); + // skipAll doesn't mark as unsynced, so do that separately + yield item.updateSynced(false); try { yield OS.File.remove( diff --git a/chrome/content/zotero/xpcom/storage/zfs.js b/chrome/content/zotero/xpcom/storage/zfs.js @@ -99,7 +99,7 @@ Zotero.Sync.Storage.Mode.ZFS.prototype = { var header; try { header = "Zotero-File-Modification-Time"; - requestData.mtime = oldChannel.getResponseHeader(header); + requestData.mtime = parseInt(oldChannel.getResponseHeader(header)); header = "Zotero-File-MD5"; requestData.md5 = oldChannel.getResponseHeader(header); header = "Zotero-File-Compressed"; @@ -131,15 +131,18 @@ Zotero.Sync.Storage.Mode.ZFS.prototype = { } // Update local metadata and stop request, skipping file download - yield Zotero.DB.executeTransaction(function* () { - if (updateHash) { - yield Zotero.Sync.Storage.Local.setSyncedHash(item.id, requestData.md5); - } - yield Zotero.Sync.Storage.Local.setSyncedModificationTime( - item.id, requestData.mtime - ); - yield Zotero.Sync.Storage.Local.setSyncState(item.id, "in_sync"); - }); + yield OS.File.setDates(path, null, new Date(requestData.mtime)); + item.attachmentSyncedModificationTime = requestData.mtime; + if (updateHash) { + item.attachmentSyncedHash = requestData.md5; + } + item.attachmentSyncState = "in_sync"; + yield item.saveTx({ skipAll: true }); + + deferred.resolve(new Zotero.Sync.Storage.Result({ + localChanges: true + })); + return false; }), onProgress: function (a, b, c) { @@ -261,7 +264,7 @@ Zotero.Sync.Storage.Mode.ZFS.prototype = { var sql = "SELECT value FROM settings WHERE setting=? AND key=?"; var values = yield Zotero.DB.columnQueryAsync(sql, ['storage', 'zfsPurge']); - if (!values) { + if (!values.length) { return false; } @@ -353,7 +356,7 @@ Zotero.Sync.Storage.Mode.ZFS.prototype = { var headers = { "Content-Type": "application/x-www-form-urlencoded" }; - var storedHash = yield Zotero.Sync.Storage.Local.getSyncedHash(item.id); + var storedHash = item.attachmentSyncedHash; //var storedModTime = yield Zotero.Sync.Storage.getSyncedModificationTime(item.id); if (storedHash) { headers["If-Match"] = storedHash; @@ -538,17 +541,17 @@ Zotero.Sync.Storage.Mode.ZFS.prototype = { Zotero.debug(fileHash); if (json.data.md5 == fileHash) { - yield Zotero.DB.executeTransaction(function* () { - yield Zotero.Sync.Storage.Local.setSyncedModificationTime( - item.id, fileModTime - ); - yield Zotero.Sync.Storage.Local.setSyncedHash(item.id, fileHash); - yield Zotero.Sync.Storage.Local.setSyncState(item.id, "in_sync"); - }); + item.attachmentSyncedModificationTime = fileModTime; + item.attachmentSyncedHash = fileHash; + item.attachmentSyncState = "in_sync"; + yield item.saveTx({ skipAll: true }); + return new Zotero.Sync.Storage.Result; } - yield Zotero.Sync.Storage.Local.setSyncState(item.id, "in_conflict"); + item.attachmentSyncState = "in_conflict"; + yield item.saveTx({ skipAll: true }); + return new Zotero.Sync.Storage.Result({ fileSyncRequired: true }); @@ -767,11 +770,12 @@ Zotero.Sync.Storage.Mode.ZFS.prototype = { _updateItemFileInfo: Zotero.Promise.coroutine(function* (item, params) { // Mark as in-sync yield Zotero.DB.executeTransaction(function* () { - yield Zotero.Sync.Storage.Local.setSyncState(item.id, "in_sync"); + // Store file mod time and hash + item.attachmentSyncedModificationTime = params.mtime; + item.attachmentSyncedHash = params.md5; + item.attachmentSyncState = "in_sync"; + yield item.save({ skipAll: true }); - // Store file mod time and hash - yield Zotero.Sync.Storage.Local.setSyncedModificationTime(item.id, params.mtime); - yield Zotero.Sync.Storage.Local.setSyncedHash(item.id, params.md5); // Update sync cache with new file metadata and version from server var json = yield Zotero.Sync.Data.Local.getCacheObject( 'item', item.libraryID, item.key, item.version @@ -933,7 +937,7 @@ Zotero.Sync.Storage.Mode.ZFS.prototype = { } // Check for conflict - if ((yield Zotero.Sync.Storage.Local.getSyncState(item.id)) + if (item.attachmentSyncState != Zotero.Sync.Storage.Local.SYNC_STATE_FORCE_UPLOAD) { if (info) { // Local file time diff --git a/chrome/content/zotero/xpcom/style.js b/chrome/content/zotero/xpcom/style.js @@ -437,7 +437,7 @@ Zotero.Styles = new function() { yield Zotero.File.putContentsAsync(destFile, style); yield Zotero.Styles.reinit(); - + // Refresh preferences windows var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]. getService(Components.interfaces.nsIWindowMediator); @@ -691,7 +691,7 @@ Zotero.Style.prototype.getCiteProc = function(locale, automaticJournalAbbreviati } try { - var citeproc = new Zotero.Cite.AsyncCiteProc( + var citeproc = new Zotero.CiteProc.CSL.Engine( new Zotero.Cite.System(automaticJournalAbbreviations), xml, locale, @@ -832,4 +832,4 @@ Zotero.Style.prototype.remove = Zotero.Promise.coroutine(function* () { } return Zotero.Styles.reinit(); -}); -\ No newline at end of file +}); diff --git a/chrome/content/zotero/xpcom/sync/syncEngine.js b/chrome/content/zotero/xpcom/sync/syncEngine.js @@ -290,7 +290,7 @@ Zotero.Sync.Data.Engine.prototype._startDownload = Zotero.Promise.coroutine(func } if (objectType == 'setting') { - let meta = yield Zotero.SyncedSettings.getMetadata(this.libraryID, key); + let meta = Zotero.SyncedSettings.getMetadata(this.libraryID, key); if (!meta) { continue; } @@ -316,7 +316,7 @@ Zotero.Sync.Data.Engine.prototype._startDownload = Zotero.Promise.coroutine(func // Conflict resolution else if (objectType == 'item') { conflicts.push({ - left: yield obj.toJSON(), + left: obj.toJSON(), right: { deleted: true } diff --git a/chrome/content/zotero/xpcom/sync/syncLocal.js b/chrome/content/zotero/xpcom/sync/syncLocal.js @@ -512,7 +512,7 @@ Zotero.Sync.Data.Local = { objectType, obj.libraryID, obj.key, obj.version ); - let jsonDataLocal = yield obj.toJSON(); + let jsonDataLocal = obj.toJSON(); // For items, check if mtime or file hash changed in metadata, // which would indicate that a remote storage sync took place and @@ -780,7 +780,8 @@ Zotero.Sync.Data.Local = { markToDownload = true; } if (markToDownload) { - yield Zotero.Sync.Storage.Local.setSyncState(item.id, "to_download"); + item.attachmentSyncState = "to_download"; + yield item.save({ skipAll: true }); } }), @@ -870,7 +871,6 @@ Zotero.Sync.Data.Local = { _saveObjectFromJSON: Zotero.Promise.coroutine(function* (obj, json, options) { try { - yield obj.loadAllData(); obj.fromJSON(json); if (!options.saveAsChanged) { obj.version = json.version; diff --git a/chrome/content/zotero/xpcom/syncedSettings.js b/chrome/content/zotero/xpcom/syncedSettings.js @@ -27,6 +27,8 @@ * @namespace */ Zotero.SyncedSettings = (function () { + var _cache = {}; + // // Public methods // @@ -34,47 +36,95 @@ Zotero.SyncedSettings = (function () { idColumn: "setting", table: "syncedSettings", - get: Zotero.Promise.coroutine(function* (libraryID, setting) { - var sql = "SELECT value FROM syncedSettings WHERE setting=? AND libraryID=?"; - var json = yield Zotero.DB.valueQueryAsync(sql, [setting, libraryID]); - if (!json) { - return false; + loadAll: Zotero.Promise.coroutine(function* (libraryID) { + Zotero.debug("Loading synced settings for library " + libraryID); + + if (!_cache[libraryID]) { + _cache[libraryID] = {}; } - return JSON.parse(json); + + var invalid = []; + + var sql = "SELECT setting, value, synced, version FROM syncedSettings " + + "WHERE libraryID=?"; + yield Zotero.DB.queryAsync( + sql, + libraryID, + { + onRow: function (row) { + var setting = row.getResultByIndex(0); + + var value = row.getResultByIndex(1); + try { + value = JSON.parse(value); + } + catch (e) { + invalid.push([libraryID, setting]); + return; + } + + _cache[libraryID][setting] = { + value, + synced: !!row.getResultByIndex(2), + version: row.getResultByIndex(3) + }; + } + } + ); + + // TODO: Delete invalid settings }), /** + * Return settings object + * + * @return {Object|null} + */ + get: function (libraryID, setting) { + if (!_cache[libraryID]) { + throw new Zotero.Exception.UnloadedDataException( + "Synced settings not loaded for library " + libraryID, + "syncedSettings" + ); + } + + if (!_cache[libraryID][setting]) { + return null; + } + + return JSON.parse(JSON.stringify(_cache[libraryID][setting].value)); + }, + + /** * Used by sync and tests * * @return {Object} - Object with 'synced' and 'version' properties */ - getMetadata: Zotero.Promise.coroutine(function* (libraryID, setting) { - var sql = "SELECT * FROM syncedSettings WHERE setting=? AND libraryID=?"; - var row = yield Zotero.DB.rowQueryAsync(sql, [setting, libraryID]); - if (!row) { - return false; + getMetadata: function (libraryID, setting) { + if (!_cache[libraryID]) { + throw new Zotero.Exception.UnloadedDataException( + "Synced settings not loaded for library " + libraryID, + "syncedSettings" + ); + } + + var o = _cache[libraryID][setting]; + if (!o) { + return null; } return { - synced: !!row.synced, - version: row.version + synced: o.synced, + version: o.version }; - }), + }, set: Zotero.Promise.coroutine(function* (libraryID, setting, value, version = 0, synced) { if (typeof value == undefined) { throw new Error("Value not provided"); } - // TODO: get rid of this once we have proper affected rows handling - var sql = "SELECT value FROM syncedSettings WHERE setting=? AND libraryID=?"; - var currentValue = yield Zotero.DB.valueQueryAsync(sql, [setting, libraryID]); - - // Make sure we can tell the difference between a - // missing setting (FALSE as returned by valueQuery()) - // and a FALSE setting (FALSE as returned by JSON.parse()) - var hasCurrentValue = currentValue !== false; - - currentValue = JSON.parse(currentValue); + var currentValue = this.get(libraryID, setting); + var hasCurrentValue = currentValue !== null; // Value hasn't changed if (value === currentValue) { @@ -93,7 +143,7 @@ Zotero.SyncedSettings = (function () { }; } - if (currentValue === false) { + if (!hasCurrentValue) { var event = 'add'; var extraData = {}; } @@ -102,6 +152,7 @@ Zotero.SyncedSettings = (function () { } synced = synced ? 1 : 0; + version = parseInt(version); if (hasCurrentValue) { var sql = "UPDATE syncedSettings SET value=?, version=?, synced=? " @@ -117,6 +168,13 @@ Zotero.SyncedSettings = (function () { sql, [setting, libraryID, JSON.stringify(value), version, synced] ); } + + _cache[libraryID][setting] = { + value, + synced: !!synced, + version + } + yield Zotero.Notifier.trigger(event, 'setting', [id], extraData); return true; }), @@ -124,22 +182,16 @@ Zotero.SyncedSettings = (function () { clear: Zotero.Promise.coroutine(function* (libraryID, setting, options) { options = options || {}; - // TODO: get rid of this once we have proper affected rows handling - var sql = "SELECT value FROM syncedSettings WHERE setting=? AND libraryID=?"; - var currentValue = yield Zotero.DB.valueQueryAsync(sql, [setting, libraryID]); - if (currentValue === false) { - return false; - } - currentValue = JSON.parse(currentValue); + var currentValue = this.get(libraryID, setting); + var hasCurrentValue = currentValue !== null; var id = libraryID + '/' + setting; var extraData = {}; extraData[id] = { - changed: {} - }; - extraData[id].changed = { - value: currentValue + changed: { + value: currentValue + } }; if (options.skipDeleteLog) { extraData[id].skipDeleteLog = true; @@ -148,6 +200,8 @@ Zotero.SyncedSettings = (function () { var sql = "DELETE FROM syncedSettings WHERE setting=? AND libraryID=?"; yield Zotero.DB.queryAsync(sql, [setting, libraryID]); + delete _cache[libraryID][setting]; + yield Zotero.Notifier.trigger('delete', 'setting', [id], extraData); return true; }) diff --git a/chrome/content/zotero/xpcom/timeline.js b/chrome/content/zotero/xpcom/timeline.js @@ -31,7 +31,6 @@ Zotero.Timeline = { yield '<data>\n'; for (let i=0; i<items.length; i++) { let item = items[i]; - yield item.loadItemData(); var date = item.getField(dateType, true, true); if (date) { let sqlDate = (dateType == 'date') ? Zotero.Date.multipartToSQL(date) : date; diff --git a/chrome/content/zotero/xpcom/translation/translate_item.js b/chrome/content/zotero/xpcom/translation/translate_item.js @@ -659,7 +659,6 @@ Zotero.Translate.ItemGetter.prototype = { "setCollection": Zotero.Promise.coroutine(function* (collection, getChildCollections) { // get items in this collection - yield collection.loadChildItems(); var items = new Set(collection.getChildItems()); if(getChildCollections) { @@ -668,7 +667,6 @@ Zotero.Translate.ItemGetter.prototype = { // get items in child collections for (let collection of this._collectionsLeft) { - yield collection.loadChildItems(); var childItems = collection.getChildItems(); childItems.forEach(item => items.add(item)); } @@ -720,7 +718,7 @@ Zotero.Translate.ItemGetter.prototype = { * Converts an attachment to array format and copies it to the export folder if desired */ "_attachmentToArray":Zotero.Promise.coroutine(function* (attachment) { - var attachmentArray = yield Zotero.Utilities.Internal.itemToExportFormat(attachment, this.legacy); + var attachmentArray = Zotero.Utilities.Internal.itemToExportFormat(attachment, this.legacy); var linkMode = attachment.attachmentLinkMode; if(linkMode != Zotero.Attachments.LINK_MODE_LINKED_URL) { var attachFile = attachment.getFile(); @@ -864,13 +862,13 @@ Zotero.Translate.ItemGetter.prototype = { var returnItemArray = yield this._attachmentToArray(returnItem); if(returnItemArray) return returnItemArray; } else { - var returnItemArray = yield Zotero.Utilities.Internal.itemToExportFormat(returnItem, this.legacy); + var returnItemArray = Zotero.Utilities.Internal.itemToExportFormat(returnItem, this.legacy); // get attachments, although only urls will be passed if exportFileData is off returnItemArray.attachments = []; var attachments = returnItem.getAttachments(); for each(var attachmentID in attachments) { - var attachment = yield Zotero.Items.getAsync(attachmentID); + var attachment = Zotero.Items.get(attachmentID); var attachmentInfo = yield this._attachmentToArray(attachment); if(attachmentInfo) { diff --git a/chrome/content/zotero/xpcom/utilities.js b/chrome/content/zotero/xpcom/utilities.js @@ -1591,8 +1591,9 @@ Zotero.Utilities = { */ "itemToCSLJSON":function(zoteroItem) { if (zoteroItem instanceof Zotero.Item) { - return Zotero.Utilities.Internal.itemToExportFormat(zoteroItem). - then(Zotero.Utilities.itemToCSLJSON); + return this.itemToCSLJSON( + Zotero.Utilities.Internal.itemToExportFormat(zoteroItem) + ); } var cslType = CSL_TYPE_MAPPINGS[zoteroItem.itemType]; diff --git a/chrome/content/zotero/xpcom/utilities_internal.js b/chrome/content/zotero/xpcom/utilities_internal.js @@ -610,44 +610,7 @@ Zotero.Utilities.Internal = { * @param {Boolean} legacy Add mappings for legacy (pre-4.0.27) translators * @return {Promise<Object>} */ - "itemToExportFormat": new function() { - return Zotero.Promise.coroutine(function* (zoteroItem, legacy) { - var item = yield zoteroItem.toJSON(); - - item.uri = Zotero.URI.getItemURI(zoteroItem); - delete item.key; - - if (!zoteroItem.isAttachment() && !zoteroItem.isNote()) { - yield zoteroItem.loadChildItems(); - - // Include attachments - item.attachments = []; - let attachments = zoteroItem.getAttachments(); - for (let i=0; i<attachments.length; i++) { - let zoteroAttachment = yield Zotero.Items.getAsync(attachments[i]), - attachment = yield zoteroAttachment.toJSON(); - if (legacy) addCompatibilityMappings(attachment, zoteroAttachment); - - item.attachments.push(attachment); - } - - // Include notes - item.notes = []; - let notes = zoteroItem.getNotes(); - for (let i=0; i<notes.length; i++) { - let zoteroNote = yield Zotero.Items.getAsync(notes[i]), - note = yield zoteroNote.toJSON(); - if (legacy) addCompatibilityMappings(note, zoteroNote); - - item.notes.push(note); - } - } - - if (legacy) addCompatibilityMappings(item, zoteroItem); - - return item; - }); - + itemToExportFormat: function (zoteroItem, legacy) { function addCompatibilityMappings(item, zoteroItem) { item.uniqueFields = {}; @@ -735,6 +698,39 @@ Zotero.Utilities.Internal = { return item; } + + var item = zoteroItem.toJSON(); + + item.uri = Zotero.URI.getItemURI(zoteroItem); + delete item.key; + + if (!zoteroItem.isAttachment() && !zoteroItem.isNote()) { + // Include attachments + item.attachments = []; + let attachments = zoteroItem.getAttachments(); + for (let i=0; i<attachments.length; i++) { + let zoteroAttachment = Zotero.Items.get(attachments[i]), + attachment = zoteroAttachment.toJSON(); + if (legacy) addCompatibilityMappings(attachment, zoteroAttachment); + + item.attachments.push(attachment); + } + + // Include notes + item.notes = []; + let notes = zoteroItem.getNotes(); + for (let i=0; i<notes.length; i++) { + let zoteroNote = Zotero.Items.get(notes[i]), + note = zoteroNote.toJSON(); + if (legacy) addCompatibilityMappings(note, zoteroNote); + + item.notes.push(note); + } + } + + if (legacy) addCompatibilityMappings(item, zoteroItem); + + return item; }, /** diff --git a/chrome/content/zotero/xpcom/zotero.js b/chrome/content/zotero/xpcom/zotero.js @@ -621,11 +621,18 @@ Components.utils.import("resource://gre/modules/osfile.jsm"); // Initialize Locate Manager Zotero.LocateManager.init(); - Zotero.Collections.init(); - Zotero.Items.init(); + yield Zotero.Collections.init(); + yield Zotero.Items.init(); yield Zotero.Searches.init(); + yield Zotero.Creators.init(); yield Zotero.Groups.init(); + let libraryIDs = Zotero.Libraries.getAll().map(x => x.libraryID); + for (let libraryID of libraryIDs) { + let library = Zotero.Libraries.get(libraryID); + yield library.loadAllDataTypes(); + } + yield Zotero.QuickCopy.init(); Zotero.Items.startEmptyTrashTimer(); diff --git a/chrome/content/zotero/zoteroPane.js b/chrome/content/zotero/zoteroPane.js @@ -860,7 +860,7 @@ var ZoteroPane = new function() }); - this.setVirtual = function (libraryID, mode, show) { + this.setVirtual = Zotero.Promise.coroutine(function* (libraryID, mode, show) { switch (mode) { case 'duplicates': var prefKey = 'duplicateLibraries'; @@ -873,7 +873,7 @@ var ZoteroPane = new function() break; default: - throw ("Invalid virtual mode '" + mode + "' in ZoteroPane.setVirtual()"); + throw new Error("Invalid virtual mode '" + mode + "'"); } try { @@ -883,10 +883,6 @@ var ZoteroPane = new function() var ids = []; } - if (!libraryID) { - libraryID = Zotero.Libraries.userLibraryID; - } - var newids = []; for (let i = 0; i < ids.length; i++) { let id = ids[i]; @@ -898,8 +894,8 @@ var ZoteroPane = new function() if (id == libraryID && !show) { continue; } - // Remove libraryIDs that no longer exist - if (id != 0 && !Zotero.Libraries.exists(id)) { + // Remove libraries that no longer exist + if (!Zotero.Libraries.exists(id)) { continue; } newids.push(id); @@ -914,22 +910,19 @@ var ZoteroPane = new function() Zotero.Prefs.set(prefKey, newids.join()); - this.collectionsView.refresh(); - - // If group is closed, open it - this.collectionsView.selectLibrary(libraryID); - row = this.collectionsView.selection.currentIndex; - if (!this.collectionsView.isContainerOpen(row)) { - this.collectionsView.toggleOpenState(row); - } + yield this.collectionsView.refresh(); // Select new row if (show) { - Zotero.Prefs.set('lastViewedFolder', lastViewedFolderID); - var row = this.collectionsView.getLastViewedRow(); - this.collectionsView.selection.select(row); + yield this.collectionsView.selectByID(lastViewedFolderID); } - } + // Select library root when hiding + else { + yield this.collectionsView.selectLibrary(libraryID); + } + + this.collectionsView.selection.selectEventsSuppressed = false; + }); this.openLookupWindow = Zotero.Promise.coroutine(function* () { @@ -1294,7 +1287,6 @@ var ZoteroPane = new function() var clearUndo = noteEditor.item ? noteEditor.item.id != item.id : false; noteEditor.parent = null; - yield item.loadNote(); noteEditor.item = item; // If loading new or different note, disable undo while we repopulate the text field @@ -1325,8 +1317,6 @@ var ZoteroPane = new function() else if (item.isAttachment()) { var attachmentBox = document.getElementById('zotero-attachment-box'); attachmentBox.mode = this.collectionsView.editable ? 'edit' : 'view'; - yield item.loadItemData(); - yield item.loadNote(); attachmentBox.item = item; document.getElementById('zotero-item-pane-content').selectedIndex = 3; @@ -1588,7 +1578,7 @@ var ZoteroPane = new function() var newItem; yield Zotero.DB.executeTransaction(function* () { - newItem = yield item.clone(null, !Zotero.Prefs.get('groups.copyTags')); + newItem = item.clone(null, !Zotero.Prefs.get('groups.copyTags')); yield newItem.save(); if (self.collectionsView.selectedTreeRow.isCollection() && newItem.isTopLevelItem()) { @@ -3641,7 +3631,6 @@ var ZoteroPane = new function() // Fall back to first attachment link if (!uri) { - yield item.loadChildItems(); let attachmentID = item.getAttachments()[0]; if (attachmentID) { let attachment = yield Zotero.Items.getAsync(attachmentID); @@ -3851,7 +3840,7 @@ var ZoteroPane = new function() }); - this.showPublicationsWizard = Zotero.Promise.coroutine(function* (items) { + this.showPublicationsWizard = function (items) { var io = { hasFiles: false, hasNotes: false, @@ -3863,14 +3852,12 @@ var ZoteroPane = new function() for (let i = 0; i < items.length; i++) { let item = items[i]; - yield item.loadItemData(); - yield item.loadChildItems(); - // Files if (!io.hasFiles && item.numAttachments()) { - let attachments = item.getAttachments(); - attachments = yield Zotero.Items.getAsync(attachments); - io.hasFiles = attachments.some(attachment => attachment.isFileAttachment()); + let attachmentIDs = item.getAttachments(); + io.hasFiles = Zotero.Items.get(attachmentIDs).some( + attachment => attachment.isFileAttachment() + ); } // Notes if (!io.hasNotes && item.numNotes()) { @@ -3887,7 +3874,7 @@ var ZoteroPane = new function() io.hasRights = allItemsHaveRights ? 'all' : (noItemsHaveRights ? 'none' : 'some'); window.openDialog('chrome://zotero/content/publicationsDialog.xul','','chrome,modal', io); return io.license ? io : false; - }); + }; /** diff --git a/components/zotero-protocol-handler.js b/components/zotero-protocol-handler.js @@ -214,7 +214,7 @@ function ZoteroProtocolHandler() { else if (combineChildItems || !results[i].isRegularItem() || results[i].numChildren() == 0) { itemsHash[results[i].id] = [items.length]; - items.push(yield results[i].toJSON({ mode: 'full' })); + items.push(results[i].toJSON({ mode: 'full' })); // Flag item as a search match items[items.length - 1].reportSearchMatch = true; } @@ -241,7 +241,6 @@ function ZoteroProtocolHandler() { } } }; - yield item.loadChildItems(); func(item.getNotes()); func(item.getAttachments()); } @@ -252,7 +251,7 @@ function ZoteroProtocolHandler() { else { for (var i in unhandledParents) { itemsHash[results[i].id] = [items.length]; - items.push(yield results[i].toJSON({ mode: 'full' })); + items.push(results[i].toJSON({ mode: 'full' })); // Flag item as a search match items[items.length - 1].reportSearchMatch = true; } @@ -264,7 +263,7 @@ function ZoteroProtocolHandler() { if (!searchItemIDs[id] && !itemsHash[id]) { var item = yield Zotero.Items.getAsync(id); itemsHash[id] = items.length; - items.push(yield item.toJSON({ mode: 'full' })); + items.push(item.toJSON({ mode: 'full' })); } } @@ -279,10 +278,10 @@ function ZoteroProtocolHandler() { }; } if (item.isNote()) { - items[itemsHash[parentID]].reportChildren.notes.push(yield item.toJSON({ mode: 'full' })); + items[itemsHash[parentID]].reportChildren.notes.push(item.toJSON({ mode: 'full' })); } if (item.isAttachment()) { - items[itemsHash[parentID]].reportChildren.attachments.push(yield item.toJSON({ mode: 'full' })); + items[itemsHash[parentID]].reportChildren.attachments.push(item.toJSON({ mode: 'full' })); } } } @@ -299,7 +298,7 @@ function ZoteroProtocolHandler() { // add on its own if (searchItemIDs[parentID]) { itemsHash[parentID] = [items.length]; - items.push(yield parentItem.toJSON({ mode: 'full' })); + items.push(parentItem.toJSON({ mode: 'full' })); items[items.length - 1].reportSearchMatch = true; } else { @@ -312,14 +311,14 @@ function ZoteroProtocolHandler() { items.push(parentItem.toJSON({ mode: 'full' })); if (item.isNote()) { items[items.length - 1].reportChildren = { - notes: [yield item.toJSON({ mode: 'full' })], + notes: [item.toJSON({ mode: 'full' })], attachments: [] }; } else if (item.isAttachment()) { items[items.length - 1].reportChildren = { notes: [], - attachments: [yield item.toJSON({ mode: 'full' })] + attachments: [item.toJSON({ mode: 'full' })] }; } } @@ -609,7 +608,6 @@ function ZoteroProtocolHandler() { if (params.controller == 'data') { switch (params.scopeObject) { case 'collections': - yield collection.loadChildItems(); var results = collection.getChildItems(); break; diff --git a/test/content/runtests.js b/test/content/runtests.js @@ -137,6 +137,8 @@ function Reporter(runner) { dump("\r" + indentStr // Dark red X for errors + "\033[31;40m" + Mocha.reporters.Base.symbols.err + " [FAIL]\033[0m" + // Trigger bell if interactive + + (Zotero.noUserInput ? "" : "\007") + " " + test.title + "\n" + indentStr + " " + err.toString() + " at\n" + err.stack.replace(/^/gm, indentStr + " ")); diff --git a/test/content/support.js b/test/content/support.js @@ -251,6 +251,33 @@ function waitForCallback(cb, interval, timeout) { } +function clickOnItemsRow(itemsView, row, button = 0) { + var x = {}; + var y = {}; + var width = {}; + var height = {}; + itemsView._treebox.getCoordsForCellItem( + row, + itemsView._treebox.columns.getNamedColumn('zotero-items-column-title'), + 'text', + x, y, width, height + ); + + // Select row to trigger multi-select + var tree = itemsView._treebox.treeBody; + var rect = tree.getBoundingClientRect(); + var x = rect.left + x.value; + var y = rect.top + y.value; + tree.dispatchEvent(new MouseEvent("mousedown", { + clientX: x, + clientY: y, + button, + detail: 1 + })); +} + + + /** * Get a default group used by all tests that want one, creating one if necessary */ @@ -352,10 +379,9 @@ function getNameProperty(objectType) { return objectType == 'item' ? 'title' : 'name'; } -var modifyDataObject = Zotero.Promise.coroutine(function* (obj, params = {}, saveOptions) { +var modifyDataObject = function (obj, params = {}, saveOptions) { switch (obj.objectType) { case 'item': - yield obj.loadItemData(); obj.setField( 'title', params.title !== undefined ? params.title : Zotero.Utilities.randomString() @@ -366,7 +392,7 @@ var modifyDataObject = Zotero.Promise.coroutine(function* (obj, params = {}, sav obj.name = params.name !== undefined ? params.name : Zotero.Utilities.randomString(); } return obj.saveTx(saveOptions); -}); +}; /** * Return a promise for the error thrown by a promise, or false if none @@ -584,7 +610,7 @@ var generateItemJSONData = Zotero.Promise.coroutine(function* generateItemJSONDa for (let itemName in items) { let zItem = yield Zotero.Items.getAsync(items[itemName].id); - jsonData[itemName] = yield zItem.toJSON(options); + jsonData[itemName] = zItem.toJSON(options); // Don't replace some fields that _always_ change (e.g. item keys) // as long as it follows expected format diff --git a/test/tests/collectionTest.js b/test/tests/collectionTest.js @@ -152,7 +152,6 @@ describe("Zotero.Collection", function() { var collection2 = yield createDataObject('collection', { parentID: collection1.id }); yield collection1.saveTx(); - yield collection1.loadChildCollections(); var childCollections = collection1.getChildCollections(); assert.lengthOf(childCollections, 1); assert.equal(childCollections[0].id, collection2.id); @@ -163,8 +162,6 @@ describe("Zotero.Collection", function() { var collection2 = yield createDataObject('collection', { parentID: collection1.id }); yield collection1.saveTx(); - yield collection1.loadChildCollections(); - collection2.parentID = false; yield collection2.save() @@ -180,7 +177,6 @@ describe("Zotero.Collection", function() { item.addToCollection(collection.key); yield item.saveTx(); - yield collection.loadChildItems(); assert.lengthOf(collection.getChildItems(), 1); }) @@ -191,7 +187,6 @@ describe("Zotero.Collection", function() { item.addToCollection(collection.key); yield item.saveTx(); - yield collection.loadChildItems(); assert.lengthOf(collection.getChildItems(), 0); }) @@ -202,7 +197,6 @@ describe("Zotero.Collection", function() { item.addToCollection(collection.key); yield item.saveTx(); - yield collection.loadChildItems(); assert.lengthOf(collection.getChildItems(false, true), 1); }) }) diff --git a/test/tests/collectionTreeViewTest.js b/test/tests/collectionTreeViewTest.js @@ -1,12 +1,13 @@ "use strict"; describe("Zotero.CollectionTreeView", function() { - var win, zp, cv; + var win, zp, cv, userLibraryID; before(function* () { win = yield loadZoteroPane(); zp = win.ZoteroPane; cv = zp.collectionsView; + userLibraryID = Zotero.Libraries.userLibraryID; }); beforeEach(function () { // TODO: Add a selectCollection() function and select a collection instead? @@ -16,31 +17,52 @@ describe("Zotero.CollectionTreeView", function() { win.close(); }); + describe("#refresh()", function () { + it("should show Duplicate Items and Unfiled Items in My Library by default", function* () { + Zotero.Prefs.clear('duplicateLibraries'); + Zotero.Prefs.clear('unfiledLibraries'); + yield cv.refresh(); + assert.ok(cv.getRowIndexByID("D" + userLibraryID)); + assert.ok(cv.getRowIndexByID("U" + userLibraryID)); + assert.equal(Zotero.Prefs.get('duplicateLibraries'), "" + userLibraryID); + assert.equal(Zotero.Prefs.get('unfiledLibraries'), "" + userLibraryID); + }); + + it("shouldn't show Duplicate Items and Unfiled Items if hidden", function* () { + Zotero.Prefs.set('duplicateLibraries', ""); + Zotero.Prefs.set('unfiledLibraries', ""); + yield cv.refresh(); + assert.isFalse(cv.getRowIndexByID("D" + userLibraryID)); + assert.isFalse(cv.getRowIndexByID("U" + userLibraryID)); + assert.strictEqual(Zotero.Prefs.get('duplicateLibraries'), ""); + assert.strictEqual(Zotero.Prefs.get('unfiledLibraries'), ""); + }); + }); + describe("collapse/expand", function () { it("should close and open My Library repeatedly", function* () { - var libraryID = Zotero.Libraries.userLibraryID; - yield cv.selectLibrary(libraryID); + yield cv.selectLibrary(userLibraryID); var row = cv.selection.currentIndex; - cv.collapseLibrary(libraryID); + cv.collapseLibrary(userLibraryID); var nextRow = cv.getRow(row + 1); assert.equal(cv.selection.currentIndex, row); assert.ok(nextRow.isSeparator()); assert.isFalse(cv.isContainerOpen(row)); - yield cv.expandLibrary(libraryID); + yield cv.expandLibrary(userLibraryID); nextRow = cv.getRow(row + 1); assert.equal(cv.selection.currentIndex, row); assert.ok(!nextRow.isSeparator()); assert.ok(cv.isContainerOpen(row)); - cv.collapseLibrary(libraryID); + cv.collapseLibrary(userLibraryID); nextRow = cv.getRow(row + 1); assert.equal(cv.selection.currentIndex, row); assert.ok(nextRow.isSeparator()); assert.isFalse(cv.isContainerOpen(row)); - yield cv.expandLibrary(libraryID); + yield cv.expandLibrary(userLibraryID); nextRow = cv.getRow(row + 1); assert.equal(cv.selection.currentIndex, row); assert.ok(!nextRow.isSeparator()); @@ -74,13 +96,13 @@ describe("Zotero.CollectionTreeView", function() { var row = cv.selection.currentIndex; var treeRow = cv.getRow(row); assert.ok(treeRow.isTrash()); - assert.equal(treeRow.ref.libraryID, Zotero.Libraries.userLibraryID); + assert.equal(treeRow.ref.libraryID, userLibraryID); }) }) describe("#selectWait()", function () { it("shouldn't hang if row is already selected", function* () { - var row = cv.getRowIndexByID("T" + Zotero.Libraries.userLibraryID); + var row = cv.getRowIndexByID("T" + userLibraryID); cv.selection.select(row); yield Zotero.Promise.delay(50); yield cv.selectWait(row); @@ -108,7 +130,7 @@ describe("Zotero.CollectionTreeView", function() { }); // Library should still be selected - assert.equal(cv.getSelectedLibraryID(), Zotero.Libraries.userLibraryID); + assert.equal(cv.getSelectedLibraryID(), userLibraryID); }); it("shouldn't select a new collection if skipSelect is passed", function* () { @@ -120,7 +142,7 @@ describe("Zotero.CollectionTreeView", function() { }); // Library should still be selected - assert.equal(cv.getSelectedLibraryID(), Zotero.Libraries.userLibraryID); + assert.equal(cv.getSelectedLibraryID(), userLibraryID); }); it("shouldn't select a modified collection", function* () { @@ -135,7 +157,7 @@ describe("Zotero.CollectionTreeView", function() { yield collection.saveTx(); // Modified collection should not be selected - assert.equal(cv.getSelectedLibraryID(), Zotero.Libraries.userLibraryID); + assert.equal(cv.getSelectedLibraryID(), userLibraryID); }); it("should maintain selection on a selected modified collection", function* () { @@ -217,8 +239,8 @@ describe("Zotero.CollectionTreeView", function() { var collectionRow = cv._rowMap["C" + collectionID]; var searchRow = cv._rowMap["S" + searchID]; - var duplicatesRow = cv._rowMap["D" + Zotero.Libraries.userLibraryID]; - var unfiledRow = cv._rowMap["U" + Zotero.Libraries.userLibraryID]; + var duplicatesRow = cv._rowMap["D" + userLibraryID]; + var unfiledRow = cv._rowMap["U" + userLibraryID]; assert.isAbove(searchRow, collectionRow); // If there's a duplicates row or an unfiled row, add before those. @@ -230,7 +252,7 @@ describe("Zotero.CollectionTreeView", function() { assert.isBelow(searchRow, unfiledRow); } else { - var trashRow = cv._rowMap["T" + Zotero.Libraries.userLibraryID]; + var trashRow = cv._rowMap["T" + userLibraryID]; assert.isBelow(searchRow, trashRow); } }) @@ -238,7 +260,7 @@ describe("Zotero.CollectionTreeView", function() { it("shouldn't select a new group", function* () { var group = yield createGroup(); // Library should still be selected - assert.equal(cv.getSelectedLibraryID(), Zotero.Libraries.userLibraryID); + assert.equal(cv.getSelectedLibraryID(), userLibraryID); }) it("should remove a group and all children", function* () { @@ -390,12 +412,6 @@ describe("Zotero.CollectionTreeView", function() { parentItemID: item.id }); - // Hack to unload relations to test proper loading - // - // Probably need a better method for this - item._loaded.relations = false; - attachment._loaded.relations = false; - var ids = (yield drop('item', 'L' + group.libraryID, [item.id])).ids; yield cv.selectLibrary(group.libraryID); @@ -413,7 +429,7 @@ describe("Zotero.CollectionTreeView", function() { // Check attachment assert.isTrue(itemsView.isContainer(0)); - yield itemsView.toggleOpenState(0); + itemsView.toggleOpenState(0); assert.equal(itemsView.rowCount, 2); treeRow = itemsView.getRow(1); assert.equal(treeRow.ref.id, ids[1]); diff --git a/test/tests/creatorsTest.js b/test/tests/creatorsTest.js @@ -0,0 +1,21 @@ +"use strict"; + +describe("Zotero.Creators", function() { + describe("#getIDFromData()", function () { + it("should create creator and cache data", function* () { + var data1 = { + firstName: "First", + lastName: "Last" + }; + var creatorID; + yield Zotero.DB.executeTransaction(function* () { + creatorID = yield Zotero.Creators.getIDFromData(data1, true); + }); + assert.typeOf(creatorID, 'number'); + var data2 = Zotero.Creators.get(creatorID); + assert.isObject(data2); + assert.propertyVal(data2, "firstName", data1.firstName); + assert.propertyVal(data2, "lastName", data1.lastName); + }); + }); +}); diff --git a/test/tests/dataObjectTest.js b/test/tests/dataObjectTest.js @@ -56,7 +56,6 @@ describe("Zotero.DataObject", function() { yield obj.saveTx(); if (type == 'item') { - yield obj.loadItemData(); obj.setField('title', Zotero.Utilities.randomString()); } else { @@ -131,7 +130,6 @@ describe("Zotero.DataObject", function() { yield obj.saveTx(); if (type == 'item') { - yield obj.loadItemData(); obj.setField('title', Zotero.Utilities.randomString()); } else { @@ -294,7 +292,7 @@ describe("Zotero.DataObject", function() { let obj = yield createDataObject(type); let libraryID = obj.libraryID; let key = obj.key; - let json = yield obj.toJSON(); + let json = obj.toJSON(); yield Zotero.Sync.Data.Local.saveCacheObjects(type, libraryID, [json]); yield obj.eraseTx(); let versions = yield Zotero.Sync.Data.Local.getCacheObjectVersions( diff --git a/test/tests/dataObjectUtilitiesTest.js b/test/tests/dataObjectUtilitiesTest.js @@ -25,11 +25,11 @@ describe("Zotero.DataObjectUtilities", function() { yield Zotero.DB.executeTransaction(function* () { var item = new Zotero.Item('book'); id1 = yield item.save(); - json1 = yield item.toJSON(); + json1 = item.toJSON(); var item = new Zotero.Item('book'); id2 = yield item.save(); - json2 = yield item.toJSON(); + json2 = item.toJSON(); }); var changes = Zotero.DataObjectUtilities.diff(json1, json2); diff --git a/test/tests/dateTest.js b/test/tests/dateTest.js @@ -1,4 +1,33 @@ describe("Zotero.Date", function() { + describe("#sqlToDate()", function () { + it("should convert an SQL local date into a JS Date object", function* () { + var d1 = new Date(); + var sqlDate = d1.getFullYear() + + '-' + + Zotero.Utilities.lpad(d1.getMonth() + 1, '0', 2) + + '-' + + Zotero.Utilities.lpad(d1.getDate(), '0', 2) + + ' ' + + Zotero.Utilities.lpad(d1.getHours(), '0', 2) + + ':' + + Zotero.Utilities.lpad(d1.getMinutes(), '0', 2) + + ':' + + Zotero.Utilities.lpad(d1.getSeconds(), '0', 2); + var offset = d1.getTimezoneOffset() * 60 * 1000; + var d2 = Zotero.Date.sqlToDate(sqlDate); + assert.equal( + Zotero.Date.sqlToDate(sqlDate).getTime(), + Math.floor(new Date().getTime() / 1000) * 1000 + ); + }) + + it("should convert an SQL UTC date into a JS Date object", function* () { + var date = "2016-02-27 22:00:00"; + date = Zotero.Date.sqlToDate(date, true); + assert.equal(date.getTime(), 1456610400000); + }) + }) + describe("#isISODate()", function () { it("should determine whether a date is an ISO 8601 date", function () { assert.ok(Zotero.Date.isISODate("2015")); diff --git a/test/tests/duplicatesTest.js b/test/tests/duplicatesTest.js @@ -0,0 +1,56 @@ +"use strict"; + +describe("Duplicate Items", function () { + var win, zp, cv; + + beforeEach(function* () { + Zotero.Prefs.clear('duplicateLibraries'); + win = yield loadZoteroPane(); + zp = win.ZoteroPane; + cv = zp.collectionsView; + + return selectLibrary(win); + }) + after(function () { + if (win) { + win.close(); + } + }); + + describe("Merging", function () { + it("should merge two items in duplicates view", function* () { + var item1 = yield createDataObject('item', { setTitle: true }); + var item2 = item1.clone(); + yield item2.saveTx(); + var uri2 = Zotero.URI.getItemURI(item2); + + var userLibraryID = Zotero.Libraries.userLibraryID; + + var selected = yield cv.selectByID('D' + userLibraryID); + assert.ok(selected); + yield waitForItemsLoad(win); + + // Select the first item, which should select both + var iv = zp.itemsView; + var row = iv.getRowIndexByID(item1.id); + assert.isNumber(row); + clickOnItemsRow(iv, row); + assert.equal(iv.selection.count, 2); + + // Click merge button + var button = win.document.getElementById('zotero-duplicates-merge-button'); + button.click(); + + yield waitForNotifierEvent('refresh', 'trash'); + + // Items should be gone + assert.isFalse(iv.getRowIndexByID(item1.id)); + assert.isFalse(iv.getRowIndexByID(item2.id)); + assert.isTrue(item2.deleted); + var rels = item1.getRelations(); + var pred = Zotero.Relations.replacedItemPredicate; + assert.property(rels, pred); + assert.equal(rels[pred], uri2); + }); + }); +}); diff --git a/test/tests/fileInterfaceTest.js b/test/tests/fileInterfaceTest.js @@ -21,7 +21,7 @@ describe("Zotero_File_Interface", function() { let childItems = importedCollection[0].getChildItems(); let savedItems = {}; for (let i=0; i<childItems.length; i++) { - let savedItem = yield childItems[i].toJSON(); + let savedItem = childItems[i].toJSON(); delete savedItem.dateAdded; delete savedItem.dateModified; delete savedItem.key; diff --git a/test/tests/itemTest.js b/test/tests/itemTest.js @@ -135,13 +135,52 @@ describe("Zotero.Item", function () { item = yield Zotero.Items.getAsync(id); assert.equal(item.getField("versionNumber"), "1.0"); }); + + it("should accept ISO 8601 dates", function* () { + var fields = { + accessDate: "2015-06-07T20:56:00Z", + dateAdded: "2015-06-07T20:57:00Z", + dateModified: "2015-06-07T20:58:00Z", + }; + var item = createUnsavedDataObject('item'); + for (let i in fields) { + item.setField(i, fields[i]); + } + assert.equal(item.getField('accessDate'), '2015-06-07 20:56:00'); + assert.equal(item.dateAdded, '2015-06-07 20:57:00'); + assert.equal(item.dateModified, '2015-06-07 20:58:00'); + }) + + it("should accept SQL dates", function* () { + var fields = { + accessDate: "2015-06-07 20:56:00", + dateAdded: "2015-06-07 20:57:00", + dateModified: "2015-06-07 20:58:00", + }; + var item = createUnsavedDataObject('item'); + for (let i in fields) { + item.setField(i, fields[i]); + item.getField(i, fields[i]); + } + }) + + it("should ignore unknown accessDate values", function* () { + var fields = { + accessDate: "foo" + }; + var item = createUnsavedDataObject('item'); + for (let i in fields) { + item.setField(i, fields[i]); + } + assert.strictEqual(item.getField('accessDate'), ''); + }) }) describe("#dateAdded", function () { it("should use current time if value was not given for a new item", function* () { var item = new Zotero.Item('book'); var id = yield item.saveTx(); - item = yield Zotero.Items.getAsync(id); + item = Zotero.Items.get(id); assert.closeTo(Zotero.Date.sqlToDate(item.dateAdded, true).getTime(), Date.now(), 2000); }) @@ -184,10 +223,9 @@ describe("Zotero.Item", function () { var item = new Zotero.Item('book'); item.dateModified = dateModified; var id = yield item.saveTx(); - item = yield Zotero.Items.getAsync(id); + item = Zotero.Items.get(id); // Save again without changing Date Modified - yield item.loadItemData(); item.setField('title', 'Test'); yield item.saveTx() @@ -199,10 +237,9 @@ describe("Zotero.Item", function () { var item = new Zotero.Item('book'); item.dateModified = dateModified; var id = yield item.saveTx(); - item = yield Zotero.Items.getAsync(id); + item = Zotero.Items.get(id); // Set Date Modified to existing value - yield item.loadItemData(); item.setField('title', 'Test'); item.dateModified = dateModified; yield item.saveTx() @@ -223,10 +260,9 @@ describe("Zotero.Item", function () { var item = new Zotero.Item('book'); item.dateModified = dateModified; var id = yield item.saveTx(); - item = yield Zotero.Items.getAsync(id); + item = Zotero.Items.get(id); // Resave with skipDateModifiedUpdate - yield item.loadItemData(); item.setField('title', 'Test'); yield item.saveTx({ skipDateModifiedUpdate: true @@ -353,8 +389,7 @@ describe("Zotero.Item", function () { var item = new Zotero.Item("journalArticle"); item.setCreators(creators); var id = yield item.saveTx(); - item = yield Zotero.Items.getAsync(id); - yield item.loadCreators(); + item = Zotero.Items.get(id); assert.sameDeepMembers(item.getCreatorsJSON(), creators); }) @@ -377,8 +412,7 @@ describe("Zotero.Item", function () { var item = new Zotero.Item("journalArticle"); item.setCreators(creators); var id = yield item.saveTx(); - item = yield Zotero.Items.getAsync(id); - yield item.loadCreators(); + item = Zotero.Items.get(id); assert.sameDeepMembers(item.getCreators(), creators); }) }) @@ -614,11 +648,8 @@ describe("Zotero.Item", function () { // File should be flagged for upload // DEBUG: Is this necessary? - assert.equal( - (yield Zotero.Sync.Storage.Local.getSyncState(item.id)), - Zotero.Sync.Storage.Local.SYNC_STATE_TO_UPLOAD - ); - assert.isNull(yield Zotero.Sync.Storage.Local.getSyncedHash(item.id)); + assert.equal(item.attachmentSyncState, Zotero.Sync.Storage.Local.SYNC_STATE_TO_UPLOAD); + assert.isNull(item.attachmentSyncedHash); }) }) @@ -686,8 +717,7 @@ describe("Zotero.Item", function () { var item = new Zotero.Item('journalArticle'); item.setTags(tags); var id = yield item.saveTx(); - item = yield Zotero.Items.getAsync(id); - yield item.loadTags(); + item = Zotero.Items.get(id); assert.sameDeepMembers(item.getTags(tags), tags); }) @@ -703,8 +733,7 @@ describe("Zotero.Item", function () { var item = new Zotero.Item('journalArticle'); item.setTags(tags); var id = yield item.saveTx(); - item = yield Zotero.Items.getAsync(id); - yield item.loadTags(); + item = Zotero.Items.get(id); item.setTags(tags); assert.isFalse(item.hasChanged()); }) @@ -721,8 +750,7 @@ describe("Zotero.Item", function () { var item = new Zotero.Item('journalArticle'); item.setTags(tags); var id = yield item.saveTx(); - item = yield Zotero.Items.getAsync(id); - yield item.loadTags(); + item = Zotero.Items.get(id); item.setTags(tags.slice(0)); yield item.saveTx(); assert.sameDeepMembers(item.getTags(tags), tags.slice(0)); @@ -825,6 +853,38 @@ describe("Zotero.Item", function () { }) }) + + describe("#multiDiff", function () { + it("should return set of alternatives for differing fields in other items", function* () { + var type = 'item'; + + var dates = ['2016-03-08 17:44:45']; + var accessDates = ['2016-03-08T18:44:45Z']; + var urls = ['http://www.example.com', 'http://example.net']; + + var obj1 = createUnsavedDataObject(type); + obj1.setField('date', '2016-03-07 12:34:56'); // different in 1 and 3, not in 2 + obj1.setField('url', 'http://example.com'); // different in all three + obj1.setField('title', 'Test'); // only in 1 + + var obj2 = createUnsavedDataObject(type); + obj2.setField('url', urls[0]); + obj2.setField('accessDate', accessDates[0]); // only in 2 + + var obj3 = createUnsavedDataObject(type); + obj3.setField('date', dates[0]); + obj3.setField('url', urls[1]); + + var alternatives = obj1.multiDiff([obj2, obj3]); + + assert.sameMembers(Object.keys(alternatives), ['url', 'date', 'accessDate']); + assert.sameMembers(alternatives.url, urls); + assert.sameMembers(alternatives.date, dates); + assert.sameMembers(alternatives.accessDate, accessDates); + }); + }); + + describe("#clone()", function () { // TODO: Expand to other data it("should copy creators", function* () { @@ -837,7 +897,7 @@ describe("Zotero.Item", function () { } ]); yield item.saveTx(); - var newItem = yield item.clone(); + var newItem = item.clone(); assert.sameDeepMembers(item.getCreators(), newItem.getCreators()); }) }) @@ -851,8 +911,8 @@ describe("Zotero.Item", function () { var item = new Zotero.Item(itemType); item.setField("title", title); var id = yield item.saveTx(); - item = yield Zotero.Items.getAsync(id); - var json = yield item.toJSON(); + item = Zotero.Items.get(id); + var json = item.toJSON(); assert.equal(json.itemType, itemType); assert.equal(json.title, title); @@ -868,13 +928,13 @@ describe("Zotero.Item", function () { item.setField("title", title); item.deleted = true; var id = yield item.saveTx(); - item = yield Zotero.Items.getAsync(id); - var json = yield item.toJSON(); + item = Zotero.Items.get(id); + var json = item.toJSON(); assert.strictEqual(json.deleted, 1); }) - it("should output attachment fields from file", function* () { + it.skip("should output attachment fields from file", function* () { var file = getTestDataDirectory(); file.append('test.png'); var item = yield Zotero.Attachments.importFromFile({ file }); @@ -888,7 +948,7 @@ describe("Zotero.Item", function () { ); }); - var json = yield item.toJSON(); + var json = item.toJSON(); assert.equal(json.linkMode, 'imported_file'); assert.equal(json.filename, 'test.png'); assert.isUndefined(json.path); @@ -905,24 +965,23 @@ describe("Zotero.Item", function () { var mtime = new Date().getTime(); var md5 = 'b32e33f529942d73bea4ed112310f804'; - yield Zotero.DB.executeTransaction(function* () { - yield Zotero.Sync.Storage.Local.setSyncedModificationTime(item.id, mtime); - yield Zotero.Sync.Storage.Local.setSyncedHash(item.id, md5); - }); + item.attachmentSyncedModificationTime = mtime; + item.attachmentSyncedHash = md5; + yield item.saveTx({ skipAll: true }); - var json = yield item.toJSON({ + var json = item.toJSON({ syncedStorageProperties: true }); assert.equal(json.mtime, mtime); assert.equal(json.md5, md5); }) - it("should output unset storage properties as null", function* () { + it.skip("should output unset storage properties as null", function* () { var item = new Zotero.Item('attachment'); item.attachmentLinkMode = 'imported_file'; item.fileName = 'test.txt'; var id = yield item.saveTx(); - var json = yield item.toJSON(); + var json = item.toJSON(); assert.isNull(json.mtime); assert.isNull(json.md5); @@ -938,7 +997,7 @@ describe("Zotero.Item", function () { item.setField("title", title); var id = yield item.saveTx(); item = yield Zotero.Items.getAsync(id); - var json = yield item.toJSON({ mode: 'full' }); + var json = item.toJSON({ mode: 'full' }); assert.equal(json.title, title); assert.equal(json.date, ""); assert.equal(json.numPages, ""); @@ -955,11 +1014,11 @@ describe("Zotero.Item", function () { item.setField("title", title); var id = yield item.saveTx(); item = yield Zotero.Items.getAsync(id); - var patchBase = yield item.toJSON(); + var patchBase = item.toJSON(); item.setField("date", date); yield item.saveTx(); - var json = yield item.toJSON({ + var json = item.toJSON({ patchBase: patchBase }) assert.isUndefined(json.itemType); @@ -978,10 +1037,10 @@ describe("Zotero.Item", function () { item.deleted = true; var id = yield item.saveTx(); item = yield Zotero.Items.getAsync(id); - var patchBase = yield item.toJSON(); + var patchBase = item.toJSON(); item.deleted = false; - var json = yield item.toJSON({ + var json = item.toJSON({ patchBase: patchBase }) assert.isUndefined(json.title); @@ -992,10 +1051,10 @@ describe("Zotero.Item", function () { item.deleted = false; var id = yield item.saveTx(); item = yield Zotero.Items.getAsync(id); - var patchBase = yield item.toJSON(); + var patchBase = item.toJSON(); item.deleted = true; - var json = yield item.toJSON({ + var json = item.toJSON({ patchBase: patchBase }) assert.isUndefined(json.title); diff --git a/test/tests/itemTreeViewTest.js b/test/tests/itemTreeViewTest.js @@ -1,31 +1,31 @@ "use strict"; describe("Zotero.ItemTreeView", function() { - var win, zp, itemsView, existingItemID; + var win, zp, cv, itemsView, existingItemID; // Load Zotero pane and select library before(function* () { win = yield loadZoteroPane(); zp = win.ZoteroPane; + cv = zp.collectionsView; var item = new Zotero.Item('book'); existingItemID = yield item.saveTx(); }); beforeEach(function* () { - yield zp.collectionsView.selectLibrary(); - yield waitForItemsLoad(win) + yield selectLibrary(win); itemsView = zp.itemsView; }) after(function () { win.close(); }); - it("shouldn't show items in trash", function* () { + it("shouldn't show items in trash in library root", function* () { var item = yield createDataObject('item', { title: "foo" }); var itemID = item.id; item.deleted = true; yield item.saveTx(); - assert.notOk(itemsView.getRowIndexByID(itemID)); + assert.isFalse(itemsView.getRowIndexByID(itemID)); }) describe("#selectItem()", function () { @@ -45,6 +45,17 @@ describe("Zotero.ItemTreeView", function() { }); }) + describe("#getCellText()", function () { + it("should return new value after edit", function* () { + var str = Zotero.Utilities.randomString(); + var item = yield createDataObject('item', { title: str }); + var row = itemsView.getRowIndexByID(item.id); + assert.equal(itemsView.getCellText(row, { id: 'zotero-items-column-title' }), str); + yield modifyDataObject(item); + assert.notEqual(itemsView.getCellText(row, { id: 'zotero-items-column-title' }), str); + }) + }) + describe("#notify()", function () { beforeEach(function () { sinon.spy(win.ZoteroPane, "itemSelected"); @@ -220,6 +231,22 @@ describe("Zotero.ItemTreeView", function() { yield Zotero.Items.erase(items.map(item => item.id)); }) + + + it("should remove items from Unfiled Items when added to a collection", function* () { + var userLibraryID = Zotero.Libraries.userLibraryID; + var collection = yield createDataObject('collection'); + var item = yield createDataObject('item', { title: "Unfiled Item" }); + yield zp.setVirtual(userLibraryID, 'unfiled', true); + var selected = yield cv.selectByID("U" + userLibraryID); + assert.ok(selected); + yield waitForItemsLoad(win); + assert.isNumber(zp.itemsView.getRowIndexByID(item.id)); + yield Zotero.DB.executeTransaction(function* () { + yield collection.addItem(item.id); + }); + assert.isFalse(zp.itemsView.getRowIndexByID(item.id)); + }); }) describe("#drop()", function () { diff --git a/test/tests/libraryTest.js b/test/tests/libraryTest.js @@ -150,6 +150,11 @@ describe("Zotero.Library", function() { yield library.saveTx(); assert.isFalse(Zotero.Libraries.isEditable(library.libraryID)); }); + + it("should initialize library after creation", function* () { + let library = yield createGroup({}); + Zotero.SyncedSettings.get(library.libraryID, "tagColors"); + }); }); describe("#erase()", function() { it("should erase a group library", function* () { diff --git a/test/tests/preferences_syncTest.js b/test/tests/preferences_syncTest.js @@ -148,7 +148,7 @@ describe("Sync Preferences", function () { var cont = yield win.Zotero_Preferences.Sync.checkUser(1, "A"); assert.isTrue(cont); - var json = yield item1.toJSON(); + var json = item1.toJSON(); var uri = json.relations[Zotero.Relations.linkedObjectPredicate][0]; assert.notInclude(uri, 'users/local'); assert.include(uri, 'users/1/publications'); diff --git a/test/tests/relatedboxTest.js b/test/tests/relatedboxTest.js @@ -71,12 +71,10 @@ describe("Related Box", function () { var item1 = yield createDataObject('item'); var item2 = yield createDataObject('item'); - yield item1.loadRelations(); item1.addRelatedItem(item2); - yield item1.save(); - yield item2.loadRelations(); + yield item1.saveTx(); item2.addRelatedItem(item1); - yield item2.save(); + yield item2.saveTx(); // Select the Related pane var tabbox = doc.getElementById('zotero-view-tabbox'); diff --git a/test/tests/searchTest.js b/test/tests/searchTest.js @@ -14,16 +14,20 @@ describe("Zotero.Search", function() { var s = new Zotero.Search; s.name = "Test"; s.addCondition('title', 'is', 'test'); + Zotero.debug("BEFORE SAVING"); + Zotero.debug(s._conditions); var id = yield s.saveTx(); + Zotero.debug("DONE SAVING"); + Zotero.debug(s._conditions); assert.typeOf(id, 'number'); // Check saved search - s = yield Zotero.Searches.getAsync(id); + s = Zotero.Searches.get(id); assert.ok(s); assert.instanceOf(s, Zotero.Search); assert.equal(s.libraryID, Zotero.Libraries.userLibraryID); assert.equal(s.name, "Test"); - yield s.loadConditions(); + Zotero.debug("GETTING CONDITIONS"); var conditions = s.getConditions(); assert.lengthOf(Object.keys(conditions), 1); assert.property(conditions, "0"); @@ -45,14 +49,12 @@ describe("Zotero.Search", function() { // Add condition s = yield Zotero.Searches.getAsync(id); - yield s.loadConditions(); s.addCondition('title', 'contains', 'foo'); var saved = yield s.saveTx(); assert.isTrue(saved); // Check saved search s = yield Zotero.Searches.getAsync(id); - yield s.loadConditions(); var conditions = s.getConditions(); assert.lengthOf(Object.keys(conditions), 2); }); @@ -69,14 +71,12 @@ describe("Zotero.Search", function() { // Remove condition s = yield Zotero.Searches.getAsync(id); - yield s.loadConditions(); s.removeCondition(0); var saved = yield s.saveTx(); assert.isTrue(saved); // Check saved search s = yield Zotero.Searches.getAsync(id); - yield s.loadConditions(); var conditions = s.getConditions(); assert.lengthOf(Object.keys(conditions), 1); assert.property(conditions, "0"); diff --git a/test/tests/storageLocalTest.js b/test/tests/storageLocalTest.js @@ -28,11 +28,10 @@ describe("Zotero.Sync.Storage.Local", function () { yield OS.File.setDates((yield item.getFilePathAsync()), null, mtime); // Mark as synced, so it will be checked - yield Zotero.DB.executeTransaction(function* () { - yield Zotero.Sync.Storage.Local.setSyncedHash(item.id, hash); - yield Zotero.Sync.Storage.Local.setSyncedModificationTime(item.id, mtime); - yield Zotero.Sync.Storage.Local.setSyncState(item.id, "in_sync"); - }); + item.attachmentSyncedModificationTime = mtime; + item.attachmentSyncedHash = hash; + item.attachmentSyncState = "in_sync"; + yield item.saveTx({ skipAll: true }); // Update mtime and contents var path = yield item.getFilePathAsync(); @@ -46,10 +45,7 @@ describe("Zotero.Sync.Storage.Local", function () { yield item.eraseTx(); assert.equal(changed, true); - assert.equal( - (yield Zotero.Sync.Storage.Local.getSyncState(item.id)), - Zotero.Sync.Storage.Local.SYNC_STATE_TO_UPLOAD - ); + assert.equal(item.attachmentSyncState, Zotero.Sync.Storage.Local.SYNC_STATE_TO_UPLOAD); }) it("should skip a file if mod time hasn't changed", function* () { @@ -59,15 +55,14 @@ describe("Zotero.Sync.Storage.Local", function () { var mtime = yield item.attachmentModificationTime; // Mark as synced, so it will be checked - yield Zotero.DB.executeTransaction(function* () { - yield Zotero.Sync.Storage.Local.setSyncedHash(item.id, hash); - yield Zotero.Sync.Storage.Local.setSyncedModificationTime(item.id, mtime); - yield Zotero.Sync.Storage.Local.setSyncState(item.id, "in_sync"); - }); + item.attachmentSyncedModificationTime = mtime; + item.attachmentSyncedHash = hash; + item.attachmentSyncState = "in_sync"; + yield item.saveTx({ skipAll: true }); var libraryID = Zotero.Libraries.userLibraryID; var changed = yield Zotero.Sync.Storage.Local.checkForUpdatedFiles(libraryID); - var syncState = yield Zotero.Sync.Storage.Local.getSyncState(item.id); + var syncState = item.attachmentSyncState; yield item.eraseTx(); @@ -84,11 +79,10 @@ describe("Zotero.Sync.Storage.Local", function () { yield OS.File.setDates((yield item.getFilePathAsync()), null, mtime); // Mark as synced, so it will be checked - yield Zotero.DB.executeTransaction(function* () { - yield Zotero.Sync.Storage.Local.setSyncedHash(item.id, hash); - yield Zotero.Sync.Storage.Local.setSyncedModificationTime(item.id, mtime); - yield Zotero.Sync.Storage.Local.setSyncState(item.id, "in_sync"); - }); + item.attachmentSyncedModificationTime = mtime; + item.attachmentSyncedHash = hash; + item.attachmentSyncState = "in_sync"; + yield item.saveTx({ skipAll: true }); // Update mtime, but not contents var path = yield item.getFilePathAsync(); @@ -96,8 +90,8 @@ describe("Zotero.Sync.Storage.Local", function () { var libraryID = Zotero.Libraries.userLibraryID; var changed = yield Zotero.Sync.Storage.Local.checkForUpdatedFiles(libraryID); - var syncState = yield Zotero.Sync.Storage.Local.getSyncState(item.id); - var syncedModTime = yield Zotero.Sync.Storage.Local.getSyncedModificationTime(item.id); + var syncState = item.attachmentSyncState; + var syncedModTime = item.attachmentSyncedModificationTime; var newModTime = yield item.attachmentModificationTime; yield item.eraseTx(); @@ -202,8 +196,8 @@ describe("Zotero.Sync.Storage.Local", function () { item3.version = 11; yield item3.saveTx(); - var json1 = yield item1.toJSON(); - var json3 = yield item3.toJSON(); + var json1 = item1.toJSON(); + var json3 = item3.toJSON(); // Change remote mtimes // Round to nearest second because OS X doesn't support ms resolution var now = Math.round(new Date().getTime() / 1000) * 1000; @@ -211,8 +205,10 @@ describe("Zotero.Sync.Storage.Local", function () { json3.mtime = now - 20000; yield Zotero.Sync.Data.Local.saveCacheObjects('item', libraryID, [json1, json3]); - yield Zotero.Sync.Storage.Local.setSyncState(item1.id, "in_conflict"); - yield Zotero.Sync.Storage.Local.setSyncState(item3.id, "in_conflict"); + item1.attachmentSyncState = "in_conflict"; + yield item1.saveTx({ skipAll: true }); + item3.attachmentSyncState = "in_conflict"; + yield item3.saveTx({ skipAll: true }); var conflicts = yield Zotero.Sync.Storage.Local.getConflicts(libraryID); assert.lengthOf(conflicts, 2); @@ -251,19 +247,17 @@ describe("Zotero.Sync.Storage.Local", function () { item3.version = 11; yield item3.saveTx(); - var json1 = yield item1.toJSON(); - var json3 = yield item3.toJSON(); + var json1 = item1.toJSON(); + var json3 = item3.toJSON(); // Change remote mtimes json1.mtime = new Date().getTime() + 10000; json3.mtime = new Date().getTime() - 10000; yield Zotero.Sync.Data.Local.saveCacheObjects('item', libraryID, [json1, json3]); - yield Zotero.Sync.Storage.Local.setSyncState( - item1.id, "in_conflict" - ); - yield Zotero.Sync.Storage.Local.setSyncState( - item3.id, "in_conflict" - ); + item1.attachmentSyncState = "in_conflict"; + yield item1.saveTx({ skipAll: true }); + item3.attachmentSyncState = "in_conflict"; + yield item3.saveTx({ skipAll: true }); var promise = waitForWindow('chrome://zotero/content/merge.xul', function (dialog) { var doc = dialog.document; @@ -305,14 +299,8 @@ describe("Zotero.Sync.Storage.Local", function () { yield Zotero.Sync.Storage.Local.resolveConflicts(libraryID); yield promise; - yield assert.eventually.equal( - Zotero.Sync.Storage.Local.getSyncState(item1.id), - Zotero.Sync.Storage.Local.SYNC_STATE_FORCE_UPLOAD - ); - yield assert.eventually.equal( - Zotero.Sync.Storage.Local.getSyncState(item3.id), - Zotero.Sync.Storage.Local.SYNC_STATE_FORCE_DOWNLOAD - ); + assert.equal(item1.attachmentSyncState, Zotero.Sync.Storage.Local.SYNC_STATE_FORCE_UPLOAD); + assert.equal(item3.attachmentSyncState, Zotero.Sync.Storage.Local.SYNC_STATE_FORCE_DOWNLOAD); }) }) diff --git a/test/tests/syncEngineTest.js b/test/tests/syncEngineTest.js @@ -239,10 +239,10 @@ describe("Zotero.Sync.Data.Engine", function () { assert.equal(Zotero.Libraries.getVersion(userLibraryID), 3); // Make sure local objects exist - var setting = yield Zotero.SyncedSettings.get(userLibraryID, "tagColors"); + var setting = Zotero.SyncedSettings.get(userLibraryID, "tagColors"); assert.lengthOf(setting, 1); assert.equal(setting[0].name, 'A'); - var settingMetadata = yield Zotero.SyncedSettings.getMetadata(userLibraryID, "tagColors"); + var settingMetadata = Zotero.SyncedSettings.getMetadata(userLibraryID, "tagColors"); assert.equal(settingMetadata.version, 2); assert.isTrue(settingMetadata.synced); @@ -283,7 +283,7 @@ describe("Zotero.Sync.Data.Engine", function () { for (let type of types) { objects[type] = [yield createDataObject(type, { setTitle: true })]; objectVersions[type] = {}; - objectResponseJSON[type] = yield Zotero.Promise.all(objects[type].map(o => o.toResponseJSON())); + objectResponseJSON[type] = objects[type].map(o => o.toResponseJSON()); } server.respond(function (req) { @@ -457,12 +457,11 @@ describe("Zotero.Sync.Data.Engine", function () { var mtime = new Date().getTime(); var md5 = '57f8a4fda823187b91e1191487b87fe6'; - yield Zotero.DB.executeTransaction(function* () { - yield Zotero.Sync.Storage.Local.setSyncedModificationTime(item.id, mtime); - yield Zotero.Sync.Storage.Local.setSyncedHash(item.id, md5); - }); + item.attachmentSyncedModificationTime = mtime; + item.attachmentSyncedHash = md5; + yield item.saveTx({ skipAll: true }); - var itemResponseJSON = yield item.toResponseJSON(); + var itemResponseJSON = item.toResponseJSON(); itemResponseJSON.version = itemResponseJSON.data.version = lastLibraryVersion; itemResponseJSON.data.mtime = mtime; itemResponseJSON.data.md5 = md5; @@ -520,7 +519,7 @@ describe("Zotero.Sync.Data.Engine", function () { for (let type of types) { objects[type] = [yield createDataObject(type, { setTitle: true })]; objectNames[type] = {}; - objectResponseJSON[type] = yield Zotero.Promise.all(objects[type].map(o => o.toResponseJSON())); + objectResponseJSON[type] = objects[type].map(o => o.toResponseJSON()); } server.respond(function (req) { @@ -569,7 +568,6 @@ describe("Zotero.Sync.Data.Engine", function () { let version = o.version; let name = objectNames[type][key]; if (type == 'item') { - yield o.loadItemData(); assert.equal(name, o.getField('title')); } else { @@ -675,7 +673,7 @@ describe("Zotero.Sync.Data.Engine", function () { { key: obj.key, version: obj.version, - data: (yield obj.toJSON()) + data: obj.toJSON() } ] ); @@ -814,7 +812,7 @@ describe("Zotero.Sync.Data.Engine", function () { yield engine._startDownload(); // Make sure objects were deleted - assert.isFalse(yield Zotero.SyncedSettings.get(userLibraryID, 'tagColors')); + assert.isNull(Zotero.SyncedSettings.get(userLibraryID, 'tagColors')); assert.isFalse(Zotero.Collections.exists(collectionID)); assert.isFalse(Zotero.Searches.exists(searchID)); assert.isFalse(Zotero.Items.exists(itemID)); @@ -903,7 +901,7 @@ describe("Zotero.Sync.Data.Engine", function () { yield engine._startDownload(); // Make sure objects weren't deleted - assert.ok(yield Zotero.SyncedSettings.get(userLibraryID, 'tagColors')); + assert.ok(Zotero.SyncedSettings.get(userLibraryID, 'tagColors')); assert.ok(Zotero.Collections.exists(collectionID)); assert.ok(Zotero.Searches.exists(searchID)); }) @@ -1214,10 +1212,10 @@ describe("Zotero.Sync.Data.Engine", function () { yield engine._fullSync(); // Check settings - var setting = yield Zotero.SyncedSettings.get(userLibraryID, "tagColors"); + var setting = Zotero.SyncedSettings.get(userLibraryID, "tagColors"); assert.lengthOf(setting, 1); assert.equal(setting[0].name, 'A'); - var settingMetadata = yield Zotero.SyncedSettings.getMetadata(userLibraryID, "tagColors"); + var settingMetadata = Zotero.SyncedSettings.getMetadata(userLibraryID, "tagColors"); assert.equal(settingMetadata.version, 2); assert.isTrue(settingMetadata.synced); diff --git a/test/tests/syncLocalTest.js b/test/tests/syncLocalTest.js @@ -105,7 +105,7 @@ describe("Zotero.Sync.Data.Local", function() { for (let type of types) { let objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(type); let obj = yield createDataObject(type); - let data = yield obj.toJSON(); + let data = obj.toJSON(); data.key = obj.key; data.version = 10; let json = { @@ -130,7 +130,7 @@ describe("Zotero.Sync.Data.Local", function() { var type = 'item'; let obj = yield createDataObject(type, { version: 5 }); - let data = yield obj.toJSON(); + let data = obj.toJSON(); yield Zotero.Sync.Data.Local.saveCacheObjects( type, libraryID, [data] ); @@ -165,7 +165,7 @@ describe("Zotero.Sync.Data.Local", function() { for (let type of types) { let obj = yield createDataObject(type, { version: 5 }); - let data = yield obj.toJSON(); + let data = obj.toJSON(); yield Zotero.Sync.Data.Local.saveCacheObjects( type, libraryID, [data] ); @@ -175,7 +175,7 @@ describe("Zotero.Sync.Data.Local", function() { let objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(type); let obj = yield createDataObject(type, { version: 10 }); - let data = yield obj.toJSON(); + let data = obj.toJSON(); yield Zotero.Sync.Data.Local.saveCacheObjects( type, libraryID, [data] ); @@ -222,11 +222,8 @@ describe("Zotero.Sync.Data.Local", function() { yield Zotero.Sync.Data.Local.processSyncCacheForObjectType( libraryID, 'item', { stopOnError: true } ); - var id = Zotero.Items.getIDFromLibraryAndKey(libraryID, key); - assert.equal( - (yield Zotero.Sync.Storage.Local.getSyncState(id)), - Zotero.Sync.Storage.Local.SYNC_STATE_TO_DOWNLOAD - ); + var item = Zotero.Items.getByLibraryAndKey(libraryID, key); + assert.equal(item.attachmentSyncState, Zotero.Sync.Storage.Local.SYNC_STATE_TO_DOWNLOAD); }) it("should mark updated attachment items for download", function* () { @@ -239,18 +236,13 @@ describe("Zotero.Sync.Data.Local", function() { yield item.saveTx(); // Set file as synced - yield Zotero.DB.executeTransaction(function* () { - yield Zotero.Sync.Storage.Local.setSyncedModificationTime( - item.id, (yield item.attachmentModificationTime) - ); - yield Zotero.Sync.Storage.Local.setSyncedHash( - item.id, (yield item.attachmentHash) - ); - yield Zotero.Sync.Storage.Local.setSyncState(item.id, "in_sync"); - }); + item.attachmentSyncedModificationTime = yield item.attachmentModificationTime; + item.attachmentSyncedHash = yield item.attachmentHash; + item.attachmentSyncState = "in_sync"; + yield item.saveTx({ skipAll: true }); // Simulate download of version with updated attachment - var json = yield item.toResponseJSON(); + var json = item.toResponseJSON(); json.version = 10; json.data.version = 10; json.data.md5 = '57f8a4fda823187b91e1191487b87fe6'; @@ -263,10 +255,7 @@ describe("Zotero.Sync.Data.Local", function() { libraryID, 'item', { stopOnError: true } ); - assert.equal( - (yield Zotero.Sync.Storage.Local.getSyncState(item.id)), - Zotero.Sync.Storage.Local.SYNC_STATE_TO_DOWNLOAD - ); + assert.equal(item.attachmentSyncState, Zotero.Sync.Storage.Local.SYNC_STATE_TO_DOWNLOAD); }) it("should ignore attachment metadata when resolving metadata conflict", function* () { @@ -276,19 +265,14 @@ describe("Zotero.Sync.Data.Local", function() { var item = yield importFileAttachment('test.png'); item.version = 5; yield item.saveTx(); - var json = yield item.toResponseJSON(); + var json = item.toResponseJSON(); yield Zotero.Sync.Data.Local.saveCacheObjects('item', libraryID, [json]); // Set file as synced - yield Zotero.DB.executeTransaction(function* () { - yield Zotero.Sync.Storage.Local.setSyncedModificationTime( - item.id, (yield item.attachmentModificationTime) - ); - yield Zotero.Sync.Storage.Local.setSyncedHash( - item.id, (yield item.attachmentHash) - ); - yield Zotero.Sync.Storage.Local.setSyncState(item.id, "in_sync"); - }); + item.attachmentSyncedModificationTime = yield item.attachmentModificationTime; + item.attachmentSyncedHash = yield item.attachmentHash; + item.attachmentSyncState = "in_sync"; + yield item.saveTx({ skipAll: true }); // Modify title locally, leaving item unsynced var newTitle = Zotero.Utilities.randomString(); @@ -307,10 +291,7 @@ describe("Zotero.Sync.Data.Local", function() { ); assert.equal(item.getField('title'), newTitle); - assert.equal( - (yield Zotero.Sync.Storage.Local.getSyncState(item.id)), - Zotero.Sync.Storage.Local.SYNC_STATE_TO_DOWNLOAD - ); + assert.equal(item.attachmentSyncState, Zotero.Sync.Storage.Local.SYNC_STATE_TO_DOWNLOAD); }) }) @@ -348,7 +329,7 @@ describe("Zotero.Sync.Data.Local", function() { ) } ); - let jsonData = yield obj.toJSON(); + let jsonData = obj.toJSON(); jsonData.key = obj.key; jsonData.version = 10; let json = { @@ -426,7 +407,7 @@ describe("Zotero.Sync.Data.Local", function() { ) } ); - let jsonData = yield obj.toJSON(); + let jsonData = obj.toJSON(); jsonData.key = obj.key; jsonData.version = 10; let json = { @@ -496,7 +477,7 @@ describe("Zotero.Sync.Data.Local", function() { // Create object, generate JSON, and delete var obj = yield createDataObject(type, { version: 10 }); - var jsonData = yield obj.toJSON(); + var jsonData = obj.toJSON(); var key = jsonData.key = obj.key; jsonData.version = 10; let json = { @@ -544,7 +525,7 @@ describe("Zotero.Sync.Data.Local", function() { // Create object, generate JSON, and delete var obj = yield createDataObject(type, { version: 10 }); - var jsonData = yield obj.toJSON(); + var jsonData = obj.toJSON(); var key = jsonData.key = obj.key; jsonData.version = 10; let json = { @@ -576,7 +557,6 @@ describe("Zotero.Sync.Data.Local", function() { obj = objectsClass.getByLibraryAndKey(libraryID, key); assert.ok(obj); - yield obj.loadItemData(); assert.equal(obj.getField('title'), jsonData.title); }) @@ -594,7 +574,7 @@ describe("Zotero.Sync.Data.Local", function() { obj.setNote(""); obj.version = 10; yield obj.saveTx(); - var jsonData = yield obj.toJSON(); + var jsonData = obj.toJSON(); var key = jsonData.key = obj.key; let json = { key: obj.key, @@ -626,7 +606,6 @@ describe("Zotero.Sync.Data.Local", function() { obj = objectsClass.getByLibraryAndKey(libraryID, key); assert.ok(obj); - yield obj.loadNote(); assert.equal(obj.getNote(), noteText2); }) }) diff --git a/test/tests/tagSelectorTest.js b/test/tests/tagSelectorTest.js @@ -4,7 +4,7 @@ describe("Tag Selector", function () { var win, doc, collectionsView; var clearTagColors = Zotero.Promise.coroutine(function* (libraryID) { - var tagColors = yield Zotero.Tags.getColors(libraryID); + var tagColors = Zotero.Tags.getColors(libraryID); for (let name of tagColors.keys()) { yield Zotero.Tags.setColor(libraryID, name, false); } @@ -155,6 +155,18 @@ describe("Tag Selector", function () { assert.equal(getRegularTags().length, 1); }) + it("should show a colored tag at the top of the list even when linked to no items", function* () { + var libraryID = Zotero.Libraries.userLibraryID; + + var tagSelector = doc.getElementById('zotero-tag-selector'); + var tagElems = tagSelector.id('tags-box').childNodes; + var count = tagElems.length; + + yield Zotero.Tags.setColor(libraryID, "Top", '#AAAAAA'); + + assert.equal(tagElems.length, count + 1); + }); + it("shouldn't re-insert a new tag that matches an existing color", function* () { var libraryID = Zotero.Libraries.userLibraryID; diff --git a/test/tests/tagsTest.js b/test/tests/tagsTest.js @@ -64,4 +64,38 @@ describe("Zotero.Tags", function () { assert.isFalse(yield Zotero.Tags.getName(tagID)); }) }) + + + describe("#setColor()", function () { + var libraryID; + + before(function* () { + libraryID = Zotero.Libraries.userLibraryID; + + // Clear library tag colors + var colors = Zotero.Tags.getColors(libraryID); + for (let color of colors.keys()) { + yield Zotero.Tags.setColor(libraryID, color); + } + }); + + it("should set color for a tag", function* () { + var aColor = '#ABCDEF'; + var bColor = '#BCDEF0'; + yield Zotero.Tags.setColor(libraryID, "A", aColor); + yield Zotero.Tags.setColor(libraryID, "B", bColor); + + var o = Zotero.Tags.getColor(libraryID, "A") + assert.equal(o.color, aColor); + assert.equal(o.position, 0); + var o = Zotero.Tags.getColor(libraryID, "B") + assert.equal(o.color, bColor); + assert.equal(o.position, 1); + + var o = Zotero.SyncedSettings.get(libraryID, 'tagColors'); + assert.isArray(o); + assert.lengthOf(o, 2); + assert.sameMembers(o.map(c => c.color), [aColor, bColor]); + }); + }); }) diff --git a/test/tests/tagsboxTest.js b/test/tests/tagsboxTest.js @@ -14,17 +14,6 @@ describe("Item Tags Box", function () { win.close(); }); - function waitForTagsBox() { - var deferred = Zotero.Promise.defer(); - var tagsbox = doc.getElementById('zotero-editpane-tags'); - var onRefresh = function (event) { - tagsbox.removeEventListener('refresh', onRefresh); - deferred.resolve(); - } - tagsbox.addEventListener('refresh', onRefresh); - return deferred.promise; - } - describe("#notify()", function () { it("should update an existing tag on rename", function* () { var tag = Zotero.Utilities.randomString(); @@ -43,7 +32,6 @@ describe("Item Tags Box", function () { var tabbox = doc.getElementById('zotero-view-tabbox'); tabbox.selectedIndex = 2; - yield waitForTagsBox(); var tagsbox = doc.getElementById('zotero-editpane-tags'); var rows = tagsbox.id('tagRows').getElementsByTagName('row'); assert.equal(rows.length, 1); @@ -77,7 +65,6 @@ describe("Item Tags Box", function () { var tabbox = doc.getElementById('zotero-view-tabbox'); tabbox.selectedIndex = 2; - yield waitForTagsBox(); var tagsbox = doc.getElementById('zotero-editpane-tags'); var rows = tagsbox.id('tagRows').getElementsByTagName('row'); @@ -108,7 +95,6 @@ describe("Item Tags Box", function () { var tabbox = doc.getElementById('zotero-view-tabbox'); tabbox.selectedIndex = 2; - yield waitForTagsBox(); var tagsbox = doc.getElementById('zotero-editpane-tags'); var rows = tagsbox.id('tagRows').getElementsByTagName('row'); assert.equal(rows.length, 1); diff --git a/test/tests/translateTest.js b/test/tests/translateTest.js @@ -64,13 +64,13 @@ function saveItemsThroughTranslator(translatorType, items) { * Convert an array of items to an object in which they are indexed by * their display titles */ -var itemsArrayToObject = Zotero.Promise.coroutine(function* itemsArrayToObject(items) { +function itemsArrayToObject(items) { var obj = {}; for (let item of items) { - obj[yield item.loadDisplayTitle(true)] = item; + obj[item.getDisplayTitle()] = item; } return obj; -}); +} const TEST_TAGS = [ "manual tag as string", @@ -175,7 +175,7 @@ describe("Zotero.Translate", function() { let newItems = yield saveItemsThroughTranslator("import", saveItems); let savedItems = {}; for (let i=0; i<newItems.length; i++) { - let savedItem = yield newItems[i].toJSON(); + let savedItem = newItems[i].toJSON(); savedItems[Zotero.ItemTypes.getName(newItems[i].itemTypeID)] = savedItem; delete savedItem.dateAdded; delete savedItem.dateModified; @@ -223,7 +223,7 @@ describe("Zotero.Translate", function() { } ]; - let newItems = yield itemsArrayToObject(yield saveItemsThroughTranslator("import", myItems)); + let newItems = itemsArrayToObject(yield saveItemsThroughTranslator("import", myItems)); let noteIDs = newItems["Test Item"].getNotes(); let note1 = yield Zotero.Items.getAsync(noteIDs[0]); assert.equal(Zotero.ItemTypes.getName(note1.itemTypeID), "note"); @@ -261,7 +261,7 @@ describe("Zotero.Translate", function() { '}')); let newItems = yield translate.translate(); assert.equal(newItems.length, 3); - newItems = yield itemsArrayToObject(newItems); + newItems = itemsArrayToObject(newItems); assert.equal(newItems["Not in Collection"].getCollections().length, 0); let parentCollection = newItems["In Parent Collection"].getCollections(); @@ -313,7 +313,7 @@ describe("Zotero.Translate", function() { "attachments":childAttachments }); - let newItems = yield itemsArrayToObject(yield saveItemsThroughTranslator("import", myItems)); + let newItems = itemsArrayToObject(yield saveItemsThroughTranslator("import", myItems)); let containedAttachments = yield Zotero.Items.getAsync(newItems["Container Item"].getAttachments()); assert.equal(containedAttachments.length, 3); @@ -447,7 +447,7 @@ describe("Zotero.Translate", function() { let newItems = yield saveItemsThroughTranslator("web", myItems); assert.equal(newItems.length, 1); - let containedAttachments = yield itemsArrayToObject(yield Zotero.Items.getAsync(newItems[0].getAttachments())); + let containedAttachments = itemsArrayToObject(yield Zotero.Items.getAsync(newItems[0].getAttachments())); let link = containedAttachments["Link to zotero.org"]; assert.equal(link.getField("url"), "http://www.zotero.org/"); diff --git a/test/tests/utilitiesTest.js b/test/tests/utilitiesTest.js @@ -179,7 +179,7 @@ describe("Zotero.Utilities", function() { let fromZoteroItem; try { - fromZoteroItem = yield Zotero.Utilities.itemToCSLJSON(item); + fromZoteroItem = Zotero.Utilities.itemToCSLJSON(item); } catch(e) { assert.fail(e, null, 'accepts Zotero Item'); } @@ -190,7 +190,7 @@ describe("Zotero.Utilities", function() { let fromExportItem; try { fromExportItem = Zotero.Utilities.itemToCSLJSON( - yield Zotero.Utilities.Internal.itemToExportFormat(item) + Zotero.Utilities.Internal.itemToExportFormat(item) ); } catch(e) { assert.fail(e, null, 'accepts Zotero export item'); @@ -205,7 +205,7 @@ describe("Zotero.Utilities", function() { note.setNote('Some note longer than 50 characters, which will become the title.'); yield note.saveTx(); - let cslJSONNote = yield Zotero.Utilities.itemToCSLJSON(note); + let cslJSONNote = Zotero.Utilities.itemToCSLJSON(note); assert.equal(cslJSONNote.type, 'article', 'note is exported as "article"'); assert.equal(cslJSONNote.title, note.getNoteTitle(), 'note title is set to Zotero pseudo-title'); })); @@ -221,7 +221,7 @@ describe("Zotero.Utilities", function() { yield attachment.saveTx(); - let cslJSONAttachment = yield Zotero.Utilities.itemToCSLJSON(attachment); + let cslJSONAttachment = Zotero.Utilities.itemToCSLJSON(attachment); assert.equal(cslJSONAttachment.type, 'article', 'attachment is exported as "article"'); assert.equal(cslJSONAttachment.title, 'Empty', 'attachment title is correct'); assert.deepEqual(cslJSONAttachment.accessed, {"date-parts":[["2001",2,3]]}, 'attachment access date is mapped correctly'); @@ -240,27 +240,27 @@ describe("Zotero.Utilities", function() { item.setField('extra', 'PMID: 12345\nPMCID:123456'); yield item.saveTx(); - let cslJSON = yield Zotero.Utilities.itemToCSLJSON(item); + let cslJSON = Zotero.Utilities.itemToCSLJSON(item); assert.equal(cslJSON.PMID, '12345', 'PMID from Extra is mapped to PMID'); assert.equal(cslJSON.PMCID, '123456', 'PMCID from Extra is mapped to PMCID'); item.setField('extra', 'PMID: 12345'); yield item.saveTx(); - cslJSON = yield Zotero.Utilities.itemToCSLJSON(item); + cslJSON = Zotero.Utilities.itemToCSLJSON(item); assert.equal(cslJSON.PMID, '12345', 'single-line entry is extracted correctly'); item.setField('extra', 'some junk: note\nPMID: 12345\nstuff in-between\nPMCID: 123456\nlast bit of junk!'); yield item.saveTx(); - cslJSON = yield Zotero.Utilities.itemToCSLJSON(item); + cslJSON = Zotero.Utilities.itemToCSLJSON(item); assert.equal(cslJSON.PMID, '12345', 'PMID from mixed Extra field is mapped to PMID'); assert.equal(cslJSON.PMCID, '123456', 'PMCID from mixed Extra field is mapped to PMCID'); item.setField('extra', 'a\n PMID: 12345\nfoo PMCID: 123456'); yield item.saveTx(); - cslJSON = yield Zotero.Utilities.itemToCSLJSON(item); + cslJSON = Zotero.Utilities.itemToCSLJSON(item); assert.isUndefined(cslJSON.PMCID, 'field label must not be preceded by other text'); assert.isUndefined(cslJSON.PMID, 'field label must not be preceded by a space'); @@ -268,7 +268,7 @@ describe("Zotero.Utilities", function() { item.setField('extra', 'something\npmid: 12345\n'); yield item.saveTx(); - cslJSON = yield Zotero.Utilities.itemToCSLJSON(item); + cslJSON = Zotero.Utilities.itemToCSLJSON(item); assert.isUndefined(cslJSON.PMID, 'field labels are case-sensitive'); })); @@ -347,7 +347,7 @@ describe("Zotero.Utilities", function() { }); let item = Zotero.Items.get(data.item.id); - let cslCreators = (yield Zotero.Utilities.itemToCSLJSON(item)).author; + let cslCreators = Zotero.Utilities.itemToCSLJSON(item).author; assert.deepEqual(cslCreators[0], creators[0].expect, 'simple name is not parsed'); assert.deepEqual(cslCreators[1], creators[1].expect, 'name with dropping and non-dropping particles is parsed'); @@ -359,6 +359,7 @@ describe("Zotero.Utilities", function() { }); describe("itemFromCSLJSON", function () { it("should stably perform itemToCSLJSON -> itemFromCSLJSON -> itemToCSLJSON", function* () { + this.timeout(10000); let data = loadSampleData('citeProcJSExport'); for (let i in data) { @@ -368,7 +369,7 @@ describe("Zotero.Utilities", function() { Zotero.Utilities.itemFromCSLJSON(item, json); yield item.saveTx(); - let newJSON = yield Zotero.Utilities.itemToCSLJSON(item); + let newJSON = Zotero.Utilities.itemToCSLJSON(item); delete newJSON.id; delete json.id; @@ -382,7 +383,7 @@ describe("Zotero.Utilities", function() { note.setNote('Some note longer than 50 characters, which will become the title.'); yield note.saveTx(); - let jsonNote = yield Zotero.Utilities.itemToCSLJSON(note); + let jsonNote = Zotero.Utilities.itemToCSLJSON(note); let item = new Zotero.Item(); Zotero.Utilities.itemFromCSLJSON(item, jsonNote); @@ -397,7 +398,7 @@ describe("Zotero.Utilities", function() { attachment.setNote('Note'); yield attachment.saveTx(); - let jsonAttachment = yield Zotero.Utilities.itemToCSLJSON(attachment); + let jsonAttachment = Zotero.Utilities.itemToCSLJSON(attachment); let item = new Zotero.Item(); Zotero.Utilities.itemFromCSLJSON(item, jsonAttachment); diff --git a/test/tests/webdavTest.js b/test/tests/webdavTest.js @@ -186,8 +186,8 @@ describe("Zotero.Sync.Storage.Mode.WebDAV", function () { var item = new Zotero.Item("attachment"); item.attachmentLinkMode = 'imported_file'; item.attachmentPath = 'storage:test.txt'; + item.attachmentSyncState = "to_download"; yield item.saveTx(); - yield Zotero.Sync.Storage.Local.setSyncState(item.id, "to_download"); setResponse({ method: "GET", @@ -217,8 +217,8 @@ describe("Zotero.Sync.Storage.Mode.WebDAV", function () { var item = new Zotero.Item("attachment"); item.attachmentLinkMode = 'imported_file'; item.attachmentPath = 'storage:test.txt'; + item.attachmentSyncState = "to_download"; yield item.saveTx(); - yield Zotero.Sync.Storage.Local.setSyncState(item.id, "to_download"); setResponse({ method: "GET", @@ -251,8 +251,8 @@ describe("Zotero.Sync.Storage.Mode.WebDAV", function () { var item = new Zotero.Item("attachment"); item.attachmentLinkMode = 'imported_file'; item.attachmentPath = 'storage:test.txt'; + item.attachmentSyncState = "to_download"; yield item.saveTx(); - yield Zotero.Sync.Storage.Local.setSyncState(item.id, "to_download"); setResponse({ method: "GET", @@ -298,8 +298,8 @@ describe("Zotero.Sync.Storage.Mode.WebDAV", function () { item.attachmentPath = 'storage:' + fileName; // TODO: Test binary data var text = Zotero.Utilities.randomString(); + item.attachmentSyncState = "to_download"; yield item.saveTx(); - yield Zotero.Sync.Storage.Local.setSyncState(item.id, "to_download"); // Create ZIP file containing above text file var tmpPath = Zotero.getTempDirectory().path; @@ -447,8 +447,8 @@ describe("Zotero.Sync.Storage.Mode.WebDAV", function () { assert.isTrue(result.syncRequired); // Check local objects - assert.equal((yield Zotero.Sync.Storage.Local.getSyncedModificationTime(item.id)), mtime); - assert.equal((yield Zotero.Sync.Storage.Local.getSyncedHash(item.id)), hash); + assert.equal(item.attachmentSyncedModificationTime, mtime); + assert.equal(item.attachmentSyncedHash, hash); assert.isFalse(item.synced); }) @@ -464,12 +464,9 @@ describe("Zotero.Sync.Storage.Mode.WebDAV", function () { var syncedModTime = Date.now() - 10000; var syncedHash = "3a2f092dd62178eb8bbfda42e07e64da"; - yield Zotero.DB.executeTransaction(function* () { - // Set an mtime in the past - yield Zotero.Sync.Storage.Local.setSyncedModificationTime(item.id, syncedModTime); - // And a different hash - yield Zotero.Sync.Storage.Local.setSyncedHash(item.id, syncedHash); - }); + item.attachmentSyncedModificationTime = syncedModTime; + item.attachmentSyncedHash = syncedHash; + yield item.saveTx({ skipAll: true }); var mtime = yield item.attachmentModificationTime; var hash = yield item.attachmentHash; @@ -507,8 +504,8 @@ describe("Zotero.Sync.Storage.Mode.WebDAV", function () { assert.isFalse(result.fileSyncRequired); // Check local objects - assert.equal((yield Zotero.Sync.Storage.Local.getSyncedModificationTime(item.id)), mtime); - assert.equal((yield Zotero.Sync.Storage.Local.getSyncedHash(item.id)), hash); + assert.equal(item.attachmentSyncedModificationTime, mtime); + assert.equal(item.attachmentSyncedHash, hash); assert.isFalse(item.synced); }) @@ -547,8 +544,8 @@ describe("Zotero.Sync.Storage.Mode.WebDAV", function () { assert.isFalse(result.syncRequired); // Check local object - assert.equal((yield Zotero.Sync.Storage.Local.getSyncedModificationTime(item.id)), mtime); - assert.equal((yield Zotero.Sync.Storage.Local.getSyncedHash(item.id)), hash); + assert.equal(item.attachmentSyncedModificationTime, mtime); + assert.equal(item.attachmentSyncedHash, hash); assert.isFalse(item.synced); }) @@ -593,15 +590,10 @@ describe("Zotero.Sync.Storage.Mode.WebDAV", function () { // Check local object // // Item should be marked as in conflict - assert.equal( - (yield Zotero.Sync.Storage.Local.getSyncState(item.id)), - Zotero.Sync.Storage.Local.SYNC_STATE_IN_CONFLICT - ); + assert.equal(item.attachmentSyncState, Zotero.Sync.Storage.Local.SYNC_STATE_IN_CONFLICT); // Synced mod time should have been changed, because that's what's shown in the // conflict dialog - assert.equal( - (yield Zotero.Sync.Storage.Local.getSyncedModificationTime(item.id)), newModTime - ); + assert.equal(item.attachmentSyncedModificationTime, newModTime); assert.isTrue(item.synced); }) }) diff --git a/test/tests/zfsTest.js b/test/tests/zfsTest.js @@ -143,8 +143,8 @@ describe("Zotero.Sync.Storage.Mode.ZFS", function () { var item = new Zotero.Item("attachment"); item.attachmentLinkMode = 'imported_file'; item.attachmentPath = 'storage:test.txt'; + item.attachmentSyncState = "to_download"; yield item.saveTx(); - yield Zotero.Sync.Storage.Local.setSyncState(item.id, "to_download"); this.httpd.registerPathHandler( `/users/1/items/${item.key}/file`, @@ -175,8 +175,8 @@ describe("Zotero.Sync.Storage.Mode.ZFS", function () { var item = new Zotero.Item("attachment"); item.attachmentLinkMode = 'imported_file'; item.attachmentPath = 'storage:test.txt'; + item.attachmentSyncState = "to_download"; yield item.saveTx(); - yield Zotero.Sync.Storage.Local.setSyncState(item.id, "to_download"); this.httpd.registerPathHandler( `/users/1/items/${item.key}/file`, @@ -208,8 +208,8 @@ describe("Zotero.Sync.Storage.Mode.ZFS", function () { item.attachmentPath = 'storage:test.txt'; // TODO: Test binary data var text = Zotero.Utilities.randomString(); + item.attachmentSyncState = "to_download"; yield item.saveTx(); - yield Zotero.Sync.Storage.Local.setSyncState(item.id, "to_download"); var mtime = "1441252524905"; var md5 = Zotero.Utilities.Internal.md5(text) @@ -553,14 +553,68 @@ describe("Zotero.Sync.Storage.Mode.ZFS", function () { assert.isFalse(result.syncRequired); // Check local objects - assert.equal((yield Zotero.Sync.Storage.Local.getSyncedModificationTime(item1.id)), mtime1); - assert.equal((yield Zotero.Sync.Storage.Local.getSyncedHash(item1.id)), hash1); + assert.equal(item1.attachmentSyncedModificationTime, mtime1); + assert.equal(item1.attachmentSyncedHash, hash1); assert.equal(item1.version, 10); - assert.equal((yield Zotero.Sync.Storage.Local.getSyncedModificationTime(item2.id)), mtime2); - assert.equal((yield Zotero.Sync.Storage.Local.getSyncedHash(item2.id)), hash2); + assert.equal(item2.attachmentSyncedModificationTime, mtime2); + assert.equal(item2.attachmentSyncedHash, hash2); assert.equal(item2.version, 15); }) + it("should update local info for remotely updated file that matches local file", function* () { + var { engine, client, caller } = yield setup(); + + var library = Zotero.Libraries.userLibrary; + library.libraryVersion = 5; + yield library.saveTx(); + library.storageDownloadNeeded = true; + + var file = getTestDataDirectory(); + file.append('test.txt'); + var item = yield Zotero.Attachments.importFromFile({ file }); + item.version = 5; + item.attachmentSyncState = "to_download"; + yield item.saveTx(); + var path = yield item.getFilePathAsync(); + yield OS.File.setDates(path, null, new Date() - 100000); + + var json = item.toJSON(); + yield Zotero.Sync.Data.Local.saveCacheObject('item', item.libraryID, json); + + var mtime = (Math.floor(new Date().getTime() / 1000) * 1000) + ""; + var md5 = Zotero.Utilities.Internal.md5(file) + + var s3Path = `pretend-s3/${item.key}`; + this.httpd.registerPathHandler( + `/users/1/items/${item.key}/file`, + { + handle: function (request, response) { + if (!request.hasHeader('Zotero-API-Key')) { + response.setStatusLine(null, 403, "Forbidden"); + return; + } + var key = request.getHeader('Zotero-API-Key'); + if (key != apiKey) { + response.setStatusLine(null, 403, "Invalid key"); + return; + } + response.setStatusLine(null, 302, "Found"); + response.setHeader("Zotero-File-Modification-Time", mtime, false); + response.setHeader("Zotero-File-MD5", md5, false); + response.setHeader("Zotero-File-Compressed", "No", false); + response.setHeader("Location", baseURL + s3Path, false); + } + } + ); + var result = yield engine.start(); + + assert.equal(item.attachmentSyncedModificationTime, mtime); + yield assert.eventually.equal(item.attachmentModificationTime, mtime); + assert.isTrue(result.localChanges); + assert.isFalse(result.remoteChanges); + assert.isFalse(result.syncRequired); + }) + it("should update local info for file that already exists on the server", function* () { var { engine, client, caller } = yield setup(); @@ -569,7 +623,7 @@ describe("Zotero.Sync.Storage.Mode.ZFS", function () { var item = yield Zotero.Attachments.importFromFile({ file: file }); item.version = 5; yield item.saveTx(); - var json = yield item.toJSON(); + var json = item.toJSON(); yield Zotero.Sync.Data.Local.saveCacheObject('item', item.libraryID, json); var mtime = yield item.attachmentModificationTime; @@ -615,8 +669,8 @@ describe("Zotero.Sync.Storage.Mode.ZFS", function () { assert.isFalse(result.syncRequired); // Check local objects - assert.equal((yield Zotero.Sync.Storage.Local.getSyncedModificationTime(item.id)), mtime); - assert.equal((yield Zotero.Sync.Storage.Local.getSyncedHash(item.id)), hash); + assert.equal(item.attachmentSyncedModificationTime, mtime); + assert.equal(item.attachmentSyncedHash, hash); assert.equal(item.version, newVersion); }) }) @@ -635,7 +689,7 @@ describe("Zotero.Sync.Storage.Mode.ZFS", function () { item.synced = true; yield item.saveTx(); - var itemJSON = yield item.toResponseJSON(); + var itemJSON = item.toResponseJSON(); itemJSON.data.mtime = yield item.attachmentModificationTime; itemJSON.data.md5 = yield item.attachmentHash; @@ -645,9 +699,8 @@ describe("Zotero.Sync.Storage.Mode.ZFS", function () { // storage directory was transferred, the mtime doesn't match, but the file was // never downloaded), but there's no difference in behavior var dbHash = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; - yield Zotero.DB.executeTransaction(function* () { - yield Zotero.Sync.Storage.Local.setSyncedHash(item.id, dbHash) - }); + item.attachmentSyncedHash = dbHash; + yield item.saveTx({ skipAll: true }); server.respond(function (req) { if (req.method == "POST" @@ -674,10 +727,7 @@ describe("Zotero.Sync.Storage.Mode.ZFS", function () { var result = yield zfs._processUploadFile({ name: item.libraryKey }); - yield assert.eventually.equal( - Zotero.Sync.Storage.Local.getSyncedHash(item.id), - (yield item.attachmentHash) - ); + assert.equal(item.attachmentSyncedHash, (yield item.attachmentHash)); assert.isFalse(result.localChanges); assert.isFalse(result.remoteChanges); assert.isFalse(result.syncRequired); @@ -697,7 +747,7 @@ describe("Zotero.Sync.Storage.Mode.ZFS", function () { yield item.saveTx(); var fileHash = yield item.attachmentHash; - var itemJSON = yield item.toResponseJSON(); + var itemJSON = item.toResponseJSON(); itemJSON.data.md5 = 'aaaaaaaaaaaaaaaaaaaaaaaa' server.respond(function (req) { @@ -725,11 +775,8 @@ describe("Zotero.Sync.Storage.Mode.ZFS", function () { var result = yield zfs._processUploadFile({ name: item.libraryKey }); - yield assert.eventually.isNull(Zotero.Sync.Storage.Local.getSyncedHash(item.id)); - yield assert.eventually.equal( - Zotero.Sync.Storage.Local.getSyncState(item.id), - Zotero.Sync.Storage.Local.SYNC_STATE_IN_CONFLICT - ); + assert.isNull(item.attachmentSyncedHash); + assert.equal(item.attachmentSyncState, Zotero.Sync.Storage.Local.SYNC_STATE_IN_CONFLICT); assert.isFalse(result.localChanges); assert.isFalse(result.remoteChanges); assert.isFalse(result.syncRequired); diff --git a/test/tests/zoteroPaneTest.js b/test/tests/zoteroPaneTest.js @@ -1,13 +1,14 @@ "use strict"; describe("ZoteroPane", function() { - var win, doc, zp; + var win, doc, zp, userLibraryID; // Load Zotero pane and select library before(function* () { win = yield loadZoteroPane(); doc = win.document; zp = win.ZoteroPane; + userLibraryID = Zotero.Libraries.userLibraryID; }); after(function () { @@ -157,8 +158,8 @@ describe("ZoteroPane", function() { item.attachmentPath = 'storage:test.txt'; // TODO: Test binary data var text = Zotero.Utilities.randomString(); + item.attachmentSyncState = "to_download"; yield item.saveTx(); - yield Zotero.Sync.Storage.Local.setSyncState(item.id, "to_download"); var mtime = "1441252524000"; var md5 = Zotero.Utilities.Internal.md5(text) @@ -201,4 +202,92 @@ describe("ZoteroPane", function() { assert.equal((yield Zotero.File.getContentsAsync(path)), text); }) }) + + + describe("#setVirtual()", function () { + var cv; + + before(function* () { + cv = zp.collectionsView; + }); + beforeEach(function () { + Zotero.Prefs.clear('duplicateLibraries'); + Zotero.Prefs.clear('unfiledLibraries'); + return selectLibrary(win); + }) + + it("should show a hidden virtual folder", function* () { + // Create unfiled, duplicate items + var title = Zotero.Utilities.randomString(); + var item1 = yield createDataObject('item', { title }); + var item2 = yield createDataObject('item', { title }); + + // Start hidden + Zotero.Prefs.set('duplicateLibraries', ""); + Zotero.Prefs.set('unfiledLibraries', ""); + yield cv.refresh(); + + // Show Duplicate Items + var id = "D" + userLibraryID; + assert.isFalse(cv.getRowIndexByID(id)); + yield zp.setVirtual(userLibraryID, 'duplicates', true); + + // Clicking should select both items + var row = cv.getRowIndexByID(id); + assert.ok(row); + assert.equal(cv.selection.currentIndex, row); + yield waitForItemsLoad(win); + var iv = zp.itemsView; + row = iv.getRowIndexByID(item1.id); + assert.isNumber(row); + clickOnItemsRow(iv, row); + assert.equal(iv.selection.count, 2); + + // Show Unfiled Items + id = "U" + userLibraryID; + assert.isFalse(cv.getRowIndexByID(id)); + yield zp.setVirtual(userLibraryID, 'unfiled', true); + assert.ok(cv.getRowIndexByID(id)); + }); + + it("should hide a virtual folder shown by default", function* () { + yield cv.refresh(); + + // Hide Duplicate Items + var id = "D" + userLibraryID; + assert.ok(yield cv.selectByID(id)); + yield zp.setVirtual(userLibraryID, 'duplicates', false); + assert.isFalse(cv.getRowIndexByID(id)); + + // Hide Unfiled Items + id = "U" + userLibraryID; + assert.ok(yield cv.selectByID(id)); + yield zp.setVirtual(userLibraryID, 'unfiled', false); + assert.isFalse(cv.getRowIndexByID(id)); + }); + + it("should hide an explicitly shown virtual folder", function* () { + // Start shown + Zotero.Prefs.set('duplicateLibraries', "" + userLibraryID); + Zotero.Prefs.set('unfiledLibraries', "" + userLibraryID); + yield cv.refresh(); + + // Hide Duplicate Items + var id = "D" + userLibraryID; + assert.ok(yield cv.selectByID(id)); + yield waitForItemsLoad(win); + yield zp.setVirtual(userLibraryID, 'duplicates', false); + assert.isFalse(cv.getRowIndexByID(id)); + assert.equal(cv.getSelectedLibraryID(), userLibraryID); + + + // Hide Unfiled Items + id = "U" + userLibraryID; + assert.ok(yield cv.selectByID(id)); + yield waitForItemsLoad(win); + yield zp.setVirtual(userLibraryID, 'unfiled', false); + assert.isFalse(cv.getRowIndexByID(id)); + assert.equal(cv.getSelectedLibraryID(), userLibraryID); + }); + }); })