From e45ca4edade1273fcb3acb0a85703868cf0a0558 Mon Sep 17 00:00:00 2001 From: Dan Stillman Date: Wed, 13 Jan 2021 00:40:13 -0500 Subject: [PATCH] Support `deleted` property for collections and searches This lays the groundwork for moving collections and searches to the trash instead of deleting them outright. We're not doing that yet, so the `deleted` property will never be set (except for items), but this will allow clients from this point forward to sync collections and searches with that property for when it's used in the future. For now, such objects will just be hidden from the collections pane as if they had been deleted. --- .../zotero/xpcom/collectionTreeView.js | 50 +++++++-- .../content/zotero/xpcom/data/collection.js | 29 ++++- .../content/zotero/xpcom/data/collections.js | 5 +- .../content/zotero/xpcom/data/dataObject.js | 42 ++++++- .../content/zotero/xpcom/data/dataObjects.js | 34 ++++++ chrome/content/zotero/xpcom/data/item.js | 23 ++-- chrome/content/zotero/xpcom/data/items.js | 24 ---- chrome/content/zotero/xpcom/data/search.js | 19 ++++ chrome/content/zotero/xpcom/data/searches.js | 6 +- chrome/content/zotero/xpcom/schema.js | 7 ++ resource/schema/userdata.sql | 16 ++- test/content/support.js | 4 +- test/tests/collectionTest.js | 26 ++++- test/tests/dataObjectTest.js | 105 +++++++++++++++++- test/tests/itemTest.js | 79 ------------- test/tests/searchTest.js | 13 +++ 16 files changed, 346 insertions(+), 136 deletions(-) diff --git a/chrome/content/zotero/xpcom/collectionTreeView.js b/chrome/content/zotero/xpcom/collectionTreeView.js index 19248dc042..5f41236157 100644 --- a/chrome/content/zotero/xpcom/collectionTreeView.js +++ b/chrome/content/zotero/xpcom/collectionTreeView.js @@ -441,10 +441,13 @@ Zotero.CollectionTreeView.prototype.notify = Zotero.Promise.coroutine(function* let row; let id = ids[0]; let rowID = "C" + id; + let selectedIndex = this.selection.count ? this.selection.currentIndex : 0; switch (type) { case 'collection': + let collection = Zotero.Collections.get(id); row = this.getRowIndexByID(rowID); + // If collection is visible if (row !== false) { // TODO: Only move if name changed let reopen = this.isContainerOpen(row); @@ -452,24 +455,52 @@ Zotero.CollectionTreeView.prototype.notify = Zotero.Promise.coroutine(function* this._closeContainer(row); } this._removeRow(row); - yield this._addSortedRow('collection', id); - yield this.selectByID(currentTreeRow.id); - if (reopen) { - let newRow = this.getRowIndexByID(rowID); - if (!this.isContainerOpen(newRow)) { - yield this.toggleOpenState(newRow); + + // Collection was moved to trash, so don't add it back + if (collection.deleted) { + this._refreshRowMap(); + this.selectAfterRowRemoval(selectedIndex); + } + else { + yield this._addSortedRow('collection', id); + yield this.selectByID(currentTreeRow.id); + if (reopen) { + let newRow = this.getRowIndexByID(rowID); + if (!this.isContainerOpen(newRow)) { + yield this.toggleOpenState(newRow); + } } } } + // If collection isn't currently visible and it isn't in the trash (because it was + // undeleted), add it (if possible without opening any containers) + else if (!collection.deleted) { + yield this._addSortedRow('collection', id); + // Invalidate parent in case it's become non-empty + let parentRow = this.getRowIndexByID("C" + collection.parentID); + if (parentRow !== false) { + this._treebox.invalidateRow(parentRow); + } + } break; case 'search': + let search = Zotero.Searches.get(id); row = this.getRowIndexByID("S" + id); if (row !== false) { // TODO: Only move if name changed this._removeRow(row); - yield this._addSortedRow('search', id); - yield this.selectByID(currentTreeRow.id); + + // Search moved to trash + if (search.deleted) { + this._refreshRowMap(); + this.selectAfterRowRemoval(selectedIndex); + } + // If search isn't in trash, add it back + else { + yield this._addSortedRow('search', id); + yield this.selectByID(currentTreeRow.id); + } } break; @@ -1381,6 +1412,9 @@ Zotero.CollectionTreeView.prototype._expandRow = Zotero.Promise.coroutine(functi // Add collections for (var i = 0, len = collections.length; i < len; i++) { + // Skip collections in trash + if (collections[i].deleted) continue; + let beforeRow = row + 1 + newRows; this._addRowToArray( rows, diff --git a/chrome/content/zotero/xpcom/data/collection.js b/chrome/content/zotero/xpcom/data/collection.js index a8eb838160..d5f583db7b 100644 --- a/chrome/content/zotero/xpcom/data/collection.js +++ b/chrome/content/zotero/xpcom/data/collection.js @@ -153,6 +153,7 @@ Zotero.Collection.prototype.loadFromRow = function(row) { // Boolean case 'synced': + case 'deleted': case 'hasChildCollections': case 'hasChildItems': val = !!val; @@ -174,9 +175,15 @@ Zotero.Collection.prototype.loadFromRow = function(row) { } -Zotero.Collection.prototype.hasChildCollections = function() { +Zotero.Collection.prototype.hasChildCollections = function (includeTrashed) { this._requireData('childCollections'); - return this._childCollections.size > 0; + if (!this._childCollections.size) { + return false; + } + if (includeTrashed) { + return this._childCollections.size > 0; + } + return !this.getChildCollections().every(c => c.deleted); } Zotero.Collection.prototype.hasChildItems = function() { @@ -328,6 +335,19 @@ Zotero.Collection.prototype._saveData = Zotero.Promise.coroutine(function* (env) }.bind(this)); } } + + if (this._changedData.deleted !== undefined) { + if (this._changedData.deleted) { + sql = "REPLACE INTO deletedCollections (collectionID) VALUES (?)"; + } + else { + sql = "DELETE FROM deletedCollections WHERE collectionID=?"; + } + yield Zotero.DB.queryAsync(sql, collectionID); + + this._clearChanged('deleted'); + this._markForReload('primaryData'); + } }); Zotero.Collection.prototype._finalizeSave = Zotero.Promise.coroutine(function* (env) { @@ -709,6 +729,7 @@ Zotero.Collection.prototype.fromJSON = function (json, options = {}) { case 'name': case 'parentCollection': case 'relations': + case 'deleted': break; default: @@ -726,6 +747,10 @@ Zotero.Collection.prototype.fromJSON = function (json, options = {}) { this.parentKey = json.parentCollection ? json.parentCollection : false; this.setRelations(json.relations || {}); + + if (json.deleted || this.deleted) { + this.deleted = !!json.deleted; + } } diff --git a/chrome/content/zotero/xpcom/data/collections.js b/chrome/content/zotero/xpcom/data/collections.js index 857161b7d4..4e0d0c94b4 100644 --- a/chrome/content/zotero/xpcom/data/collections.js +++ b/chrome/content/zotero/xpcom/data/collections.js @@ -40,6 +40,8 @@ Zotero.Collections = function() { version: "O.version", synced: "O.synced", + deleted: "DC.collectionID IS NOT NULL AS deleted", + parentID: "O.parentCollectionID AS parentID", parentKey: "CP.key AS parentKey", @@ -51,7 +53,8 @@ Zotero.Collections = function() { this._primaryDataSQLFrom = "FROM collections O " - + "LEFT JOIN collections CP ON (O.parentCollectionID=CP.collectionID)"; + + "LEFT JOIN deletedCollections DC ON (O.collectionID=DC.collectionID)" + + "LEFT JOIN collections CP ON (O.parentCollectionID=CP.collectionID) "; this._relationsTable = "collectionRelations"; diff --git a/chrome/content/zotero/xpcom/data/dataObject.js b/chrome/content/zotero/xpcom/data/dataObject.js index c7c8d04074..ba9f7fe3e5 100644 --- a/chrome/content/zotero/xpcom/data/dataObject.js +++ b/chrome/content/zotero/xpcom/data/dataObject.js @@ -49,6 +49,7 @@ Zotero.DataObject = function () { this._identified = false; this._parentID = null; this._parentKey = null; + this._deleted = null; this._relations = []; @@ -98,6 +99,33 @@ Zotero.defineProperty(Zotero.DataObject.prototype, '_canHaveParent', { value: true }); +// Define boolean properties +for (let name of ['deleted']) { + let prop = '_' + name; + Zotero.defineProperty(Zotero.DataObject.prototype, name, { + get: function() { + if (!this.id) { + return false; + } + var val = this._getLatestField(name); + if (this[prop] !== null) { + return this[prop]; + } + this._requireData('primaryData'); + }, + set: function(val) { + val = !!val; + var oldVal = this._getLatestField(name); + if (oldVal == val) { + Zotero.debug(Zotero.Utilities.capitalize(name) + + ` state hasn't changed for ${this._objectType} ${this.id}`); + return; + } + this._markFieldChange(name, val); + } + }); +} + Zotero.defineProperty(Zotero.DataObject.prototype, 'ObjectsClass', { get: function() { return this._ObjectsClass; } }); @@ -721,7 +749,7 @@ Zotero.DataObject.prototype._getLatestField = function (field) { */ Zotero.DataObject.prototype._markFieldChange = function (field, value) { // New method (changedData) - if (field == 'tags') { + if (['deleted', 'tags'].includes(field)) { if (Array.isArray(value)) { this._changedData[field] = [...value]; } @@ -1008,6 +1036,13 @@ Zotero.DataObject.prototype._saveData = function (env) { env.sqlColumns.push('clientDateModified'); env.sqlValues.push(Zotero.DB.transactionDateTime); } + + if (!env.options.skipNotifier && this._changedData.deleted !== undefined) { + Zotero.Notifier.queue('refresh', 'trash', this.libraryID, {}, env.options.notifierQueue); + if (!env.isNew && this._changedData.deleted) { + Zotero.Notifier.queue('trash', this._objectType, this.id, {}, env.options.notifierQueue); + } + } }; Zotero.DataObject.prototype._finalizeSave = Zotero.Promise.coroutine(function* (env) { @@ -1291,6 +1326,11 @@ Zotero.DataObject.prototype._preToJSON = function (options) { } Zotero.DataObject.prototype._postToJSON = function (env) { + var deleted = this._getLatestField('deleted'); + if (deleted || env.options.mode == 'full') { + env.obj.deleted = !!deleted; + } + if (env.mode == 'patch') { env.obj = Zotero.DataObjectUtilities.patch(env.options.patchBase, env.obj); } diff --git a/chrome/content/zotero/xpcom/data/dataObjects.js b/chrome/content/zotero/xpcom/data/dataObjects.js index 977f434b9c..b0bca903a3 100644 --- a/chrome/content/zotero/xpcom/data/dataObjects.js +++ b/chrome/content/zotero/xpcom/data/dataObjects.js @@ -231,6 +231,40 @@ Zotero.DataObjects.prototype.getLoaded = function () { } +/** + * Return objects in the trash + * + * @param {Integer} libraryID - Library to search + * @param {Boolean} [asIDs] - Return object ids instead of objects + * @param {Integer} [days] + * @param {Integer} [limit] + * @return {Promise} + */ +Zotero.DataObjects.prototype.getDeleted = async function (libraryID, asIDs, days, limit) { + var sql = `SELECT ${this._ZDO_id} FROM ${this._ZDO_table} ` + + `JOIN deleted${this._ZDO_Objects} USING (${this._ZDO_id}) ` + + "WHERE libraryID=?"; + var params = [libraryID]; + if (days) { + sql += " AND dateDeleted <= DATE('NOW', '-" + parseInt(days) + " DAYS')"; + } + if (limit) { + sql += " LIMIT ?"; + params.push(limit); + } + var ids = await Zotero.DB.columnQueryAsync(sql, params); + if (!ids.length) { + return []; + } + if (asIDs) { + return ids; + } + return this.getAsync(ids); +}; + + + + Zotero.DataObjects.prototype.getAllIDs = function (libraryID) { var sql = `SELECT ${this._ZDO_id} FROM ${this._ZDO_table} WHERE libraryID=?`; return Zotero.DB.columnQueryAsync(sql, [libraryID]); diff --git a/chrome/content/zotero/xpcom/data/item.js b/chrome/content/zotero/xpcom/data/item.js index 3c44122b67..43b763a4c2 100644 --- a/chrome/content/zotero/xpcom/data/item.js +++ b/chrome/content/zotero/xpcom/data/item.js @@ -68,7 +68,6 @@ Zotero.Item = function(itemTypeOrID) { this._bestAttachmentState = null; this._fileExists = null; - this._deleted = null; this._hasNote = null; this._noteAccessTime = null; @@ -1131,7 +1130,7 @@ Zotero.Item.prototype.removeCreator = function(orderIndex, allowMissing) { // Define boolean properties -for (let name of ['deleted', 'inPublications']) { +for (let name of ['inPublications']) { let prop = '_' + name; Zotero.defineProperty(Zotero.Item.prototype, name, { get: function() { @@ -1517,8 +1516,8 @@ Zotero.Item.prototype._saveData = Zotero.Promise.coroutine(function* (env) { } // Trashed status - if (this._changed.deleted) { - if (this._deleted) { + if (this._changedData.deleted !== undefined) { + if (this._changedData.deleted) { sql = "REPLACE INTO deletedItems (itemID) VALUES (?)"; } else { @@ -1551,7 +1550,7 @@ Zotero.Item.prototype._saveData = Zotero.Promise.coroutine(function* (env) { // Refresh trash if (!env.options.skipNotifier) { Zotero.Notifier.queue('refresh', 'trash', this.libraryID, {}, env.options.notifierQueue); - if (this._deleted) { + if (this._changedData.deleted) { Zotero.Notifier.queue('trash', 'item', this.id, {}, env.options.notifierQueue); } } @@ -1559,6 +1558,9 @@ Zotero.Item.prototype._saveData = Zotero.Promise.coroutine(function* (env) { if (parentItemID) { reloadParentChildItems[parentItemID] = true; } + + this._clearChanged('deleted'); + this._markForReload('primaryData'); } if (this._changed.inPublications) { @@ -3666,8 +3668,8 @@ Zotero.DataObject.prototype.setDeleted = Zotero.Promise.coroutine(function* (del this._deleted = !!deleted; - if (this._changed.deleted) { - delete this._changed.deleted; + if (this._changedData.deleted !== undefined) { + delete this._changedData.deleted; } }); @@ -4636,13 +4638,6 @@ Zotero.Item.prototype.toJSON = function (options = {}) { obj.inPublications = this._inPublications; } - // Deleted - let deleted = this.deleted; - if (deleted || mode == 'full') { - // Match what APIv3 returns, though it would be good to change this - obj.deleted = deleted ? 1 : 0; - } - // Relations obj.relations = this.getRelations(); diff --git a/chrome/content/zotero/xpcom/data/items.js b/chrome/content/zotero/xpcom/data/items.js index e2cfa7a273..bfbf478dd1 100644 --- a/chrome/content/zotero/xpcom/data/items.js +++ b/chrome/content/zotero/xpcom/data/items.js @@ -94,30 +94,6 @@ Zotero.Items = function() { }); - /** - * Return items marked as deleted - * - * @param {Integer} libraryID - Library to search - * @param {Boolean} [asIDs] - Return itemIDs instead of Zotero.Item objects - * @return {Promise} - */ - this.getDeleted = Zotero.Promise.coroutine(function* (libraryID, asIDs, days) { - var sql = "SELECT itemID FROM items JOIN deletedItems USING (itemID) " - + "WHERE libraryID=?"; - if (days) { - sql += " AND dateDeleted<=DATE('NOW', '-" + parseInt(days) + " DAYS')"; - } - var ids = yield Zotero.DB.columnQueryAsync(sql, [libraryID]); - if (!ids.length) { - return []; - } - if (asIDs) { - return ids; - } - return this.getAsync(ids); - }); - - /** * Returns all items in a given library * diff --git a/chrome/content/zotero/xpcom/data/search.js b/chrome/content/zotero/xpcom/data/search.js index 3463669584..eca9b4aa43 100644 --- a/chrome/content/zotero/xpcom/data/search.js +++ b/chrome/content/zotero/xpcom/data/search.js @@ -136,6 +136,7 @@ Zotero.Search.prototype.loadFromRow = function (row) { // Boolean case 'synced': + case 'deleted': val = !!val; break; @@ -219,6 +220,20 @@ Zotero.Search.prototype._saveData = Zotero.Promise.coroutine(function* (env) { i++; } } + + // Trashed status + if (this._changedData.deleted !== undefined) { + if (this._changedData.deleted) { + sql = "INSERT OR IGNORE INTO deletedSearches (savedSearchID) VALUES (?)"; + } + else { + sql = "DELETE FROM deletedSearches WHERE savedSearchID=?"; + } + yield Zotero.DB.queryAsync(sql, searchID); + + this._clearChanged('deleted'); + this._markForReload('primaryData'); + } }); Zotero.Search.prototype._finalizeSave = Zotero.Promise.coroutine(function* (env) { @@ -844,6 +859,10 @@ Zotero.Search.prototype.fromJSON = function (json, options = {}) { condition.value ); } + + if (json.deleted || this.deleted) { + this.deleted = !!json.deleted; + } } diff --git a/chrome/content/zotero/xpcom/data/searches.js b/chrome/content/zotero/xpcom/data/searches.js index b4c21c41e0..173aa0c593 100644 --- a/chrome/content/zotero/xpcom/data/searches.js +++ b/chrome/content/zotero/xpcom/data/searches.js @@ -36,10 +36,12 @@ Zotero.Searches = function() { libraryID: "O.libraryID", key: "O.key", version: "O.version", - synced: "O.synced" + synced: "O.synced", + deleted: "DS.savedSearchID IS NOT NULL AS deleted", } - this._primaryDataSQLFrom = "FROM savedSearches O"; + this._primaryDataSQLFrom = "FROM savedSearches O " + + "LEFT JOIN deletedSearches DS ON (O.savedSearchID=DS.savedSearchID)"; this.init = Zotero.Promise.coroutine(function* () { yield Zotero.DataObjects.prototype.init.apply(this); diff --git a/chrome/content/zotero/xpcom/schema.js b/chrome/content/zotero/xpcom/schema.js index e1c0c6f55e..fb65e1688b 100644 --- a/chrome/content/zotero/xpcom/schema.js +++ b/chrome/content/zotero/xpcom/schema.js @@ -3193,6 +3193,13 @@ Zotero.Schema = new function(){ yield Zotero.DB.queryAsync("UPDATE itemNotes SET parentItemID=NULL WHERE itemID=parentItemID"); } + else if (i == 111) { + yield Zotero.DB.queryAsync("CREATE TABLE deletedCollections (\n collectionID INTEGER PRIMARY KEY,\n dateDeleted DEFAULT CURRENT_TIMESTAMP NOT NULL,\n FOREIGN KEY (collectionID) REFERENCES collections(collectionID) ON DELETE CASCADE\n)"); + yield Zotero.DB.queryAsync("CREATE INDEX deletedCollections_dateDeleted ON deletedCollections(dateDeleted)"); + yield Zotero.DB.queryAsync("CREATE TABLE deletedSearches (\n savedSearchID INTEGER PRIMARY KEY,\n dateDeleted DEFAULT CURRENT_TIMESTAMP NOT NULL,\n FOREIGN KEY (savedSearchID) REFERENCES savedSearches(savedSearchID) ON DELETE CASCADE\n)"); + yield Zotero.DB.queryAsync("CREATE INDEX deletedSearches_dateDeleted ON deletedSearches(dateDeleted)"); + } + // If breaking compatibility or doing anything dangerous, clear minorUpdateFrom } diff --git a/resource/schema/userdata.sql b/resource/schema/userdata.sql index 5af9c569a9..3ddc70f30a 100644 --- a/resource/schema/userdata.sql +++ b/resource/schema/userdata.sql @@ -1,4 +1,4 @@ --- 110 +-- 111 -- Copyright (c) 2009 Center for History and New Media -- George Mason University, Fairfax, Virginia, USA @@ -241,6 +241,13 @@ CREATE TABLE savedSearchConditions ( FOREIGN KEY (savedSearchID) REFERENCES savedSearches(savedSearchID) ON DELETE CASCADE ); +CREATE TABLE deletedCollections ( + collectionID INTEGER PRIMARY KEY, + dateDeleted DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (collectionID) REFERENCES collections(collectionID) ON DELETE CASCADE +); +CREATE INDEX deletedCollections_dateDeleted ON deletedCollections(dateDeleted); + CREATE TABLE deletedItems ( itemID INTEGER PRIMARY KEY, dateDeleted DEFAULT CURRENT_TIMESTAMP NOT NULL, @@ -248,6 +255,13 @@ CREATE TABLE deletedItems ( ); CREATE INDEX deletedItems_dateDeleted ON deletedItems(dateDeleted); +CREATE TABLE deletedSearches ( + savedSearchID INTEGER PRIMARY KEY, + dateDeleted DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (savedSearchID) REFERENCES savedSearches(savedSearchID) ON DELETE CASCADE +); +CREATE INDEX deletedSearches_dateDeleted ON deletedItems(dateDeleted); + CREATE TABLE libraries ( libraryID INTEGER PRIMARY KEY, type TEXT NOT NULL, diff --git a/test/content/support.js b/test/content/support.js index 6d4379b05b..02ac0796af 100644 --- a/test/content/support.js +++ b/test/content/support.js @@ -431,12 +431,12 @@ function createUnsavedDataObject(objectType, params = {}) { throw new Error("Object type not provided"); } - var allowedParams = ['libraryID', 'parentID', 'parentKey', 'synced', 'version']; + var allowedParams = ['libraryID', 'parentID', 'parentKey', 'synced', 'version', 'deleted']; var itemType; if (objectType == 'item' || objectType == 'feedItem') { itemType = params.itemType || 'book'; - allowedParams.push('deleted', 'dateAdded', 'dateModified'); + allowedParams.push('dateAdded', 'dateModified'); } if (objectType == 'item') { allowedParams.push('inPublications'); diff --git a/test/tests/collectionTest.js b/test/tests/collectionTest.js index ec60aedc38..6304d8a41c 100644 --- a/test/tests/collectionTest.js +++ b/test/tests/collectionTest.js @@ -168,7 +168,31 @@ describe("Zotero.Collection", function() { collection2.parentKey = collection3.key; yield collection2.saveTx(); assert.isFalse(collection1.hasChildCollections()); - }) + }); + + it("should return false if all child collections are moved to trash", async function () { + var collection1 = await createDataObject('collection'); + var collection2 = await createDataObject('collection', { parentID: collection1.id }); + var collection3 = await createDataObject('collection', { parentID: collection1.id }); + + assert.isTrue(collection1.hasChildCollections()); + collection2.deleted = true; + await collection2.saveTx(); + assert.isTrue(collection1.hasChildCollections()); + collection3.deleted = true; + await collection3.saveTx(); + assert.isFalse(collection1.hasChildCollections()); + }); + + it("should return true if child collection is in trash and includeTrashed is true", async function () { + var collection1 = await createDataObject('collection'); + var collection2 = await createDataObject('collection', { parentID: collection1.id }); + + assert.isTrue(collection1.hasChildCollections(true)); + collection2.deleted = true; + await collection2.saveTx(); + assert.isTrue(collection1.hasChildCollections(true)); + }); }) describe("#getChildCollections()", function () { diff --git a/test/tests/dataObjectTest.js b/test/tests/dataObjectTest.js index ac645be3bb..8b2c5a3a83 100644 --- a/test/tests/dataObjectTest.js +++ b/test/tests/dataObjectTest.js @@ -160,6 +160,35 @@ describe("Zotero.DataObject", function() { }); }) + + describe("#deleted", function () { + it("should set trash status", async function () { + for (let type of types) { + let plural = Zotero.DataObjectUtilities.getObjectTypePlural(type) + let pluralClass = Zotero[Zotero.Utilities.capitalize(plural)]; + + // Set to true + var obj = await createDataObject(type); + assert.isFalse(obj.deleted, type); + obj.deleted = true; + // Sanity check for itemsTest#trash() + if (type == 'item') { + assert.isTrue(obj._changedData.deleted, type); + } + await obj.saveTx(); + var id = obj.id; + await pluralClass.reload(id, false, true); + assert.isTrue(obj.deleted, type); + + // Set to false + obj.deleted = false; + await obj.saveTx(); + await pluralClass.reload(id, false, true); + assert.isFalse(obj.deleted, type); + } + }); + }); + describe("#loadPrimaryData()", function () { it("should load unloaded primary data if partially set", function* () { var objs = {}; @@ -574,5 +603,79 @@ describe("Zotero.DataObject", function() { assert.equal(item1.getField('dateModified'), dateModified); }) }) - }) + }); + + describe("#fromJSON()", function () { + it("should remove object from trash if 'deleted' property not provided", async function () { + for (let type of types) { + let obj = await createDataObject(type, { deleted: true }); + + assert.isTrue(obj.deleted, type); + + let json = obj.toJSON(); + delete json.deleted; + + obj.fromJSON(json); + await obj.saveTx(); + + assert.isFalse(obj.deleted, type); + } + }); + }); + + describe("#toJSON()", function () { + it("should output 'deleted' as true", function () { + for (let type of types) { + let obj = createUnsavedDataObject(type); + obj.deleted = true; + let json = obj.toJSON(); + assert.isTrue(json.deleted, type); + } + }); + + it("shouldn't include 'deleted' if not set in default mode", function () { + for (let type of types) { + let obj = createUnsavedDataObject(type); + let json = obj.toJSON(); + assert.notProperty(json, 'deleted', type); + } + }); + + describe("'patch' mode", function () { + it("should include changed 'deleted' field", async function () { + for (let type of types) { + let plural = Zotero.DataObjectUtilities.getObjectTypePlural(type) + let pluralClass = Zotero[Zotero.Utilities.capitalize(plural)]; + + // True to false + let obj = createUnsavedDataObject(type) + obj.deleted = true; + let id = await obj.saveTx(); + obj = await pluralClass.getAsync(id); + let patchBase = obj.toJSON(); + + obj.deleted = false; + let json = obj.toJSON({ + patchBase: patchBase + }) + assert.isUndefined(json.title, type); + assert.isFalse(json.deleted, type); + + // False to true + obj = createUnsavedDataObject(type); + obj.deleted = false; + id = await obj.saveTx(); + obj = await pluralClass.getAsync(id); + patchBase = obj.toJSON(); + + obj.deleted = true; + json = obj.toJSON({ + patchBase: patchBase + }) + assert.isUndefined(json.title, type); + assert.isTrue(json.deleted, type); + } + }); + }); + }); }) diff --git a/test/tests/itemTest.js b/test/tests/itemTest.js index a6d29c1883..6fb0d18ca6 100644 --- a/test/tests/itemTest.js +++ b/test/tests/itemTest.js @@ -321,28 +321,6 @@ describe("Zotero.Item", function () { }) }) - describe("#deleted", function () { - it("should be set to true after save", function* () { - var item = yield createDataObject('item'); - item.deleted = true; - // Sanity check for itemsTest#trash() - assert.isTrue(item._changed.deleted); - yield item.saveTx(); - assert.ok(item.deleted); - }) - - it("should be set to false after save", function* () { - var collection = yield createDataObject('collection'); - var item = createUnsavedDataObject('item'); - item.deleted = true; - yield item.saveTx(); - - item.deleted = false; - yield item.saveTx(); - assert.isFalse(item.deleted); - }) - }) - describe("#inPublications", function () { it("should add item to publications table", function* () { var item = yield createDataObject('item'); @@ -1500,20 +1478,6 @@ describe("Zotero.Item", function () { assert.isUndefined(json.numPages); }) - it("should output 'deleted' as 1", function* () { - var itemType = "book"; - var title = "Test"; - - var item = new Zotero.Item(itemType); - item.setField("title", title); - item.deleted = true; - var id = yield item.saveTx(); - item = Zotero.Items.get(id); - var json = item.toJSON(); - - assert.strictEqual(json.deleted, 1); - }) - it.skip("should output attachment fields from file", function* () { var file = getTestDataDirectory(); file.append('test.png'); @@ -1662,36 +1626,6 @@ describe("Zotero.Item", function () { assert.isUndefined(json.tags); }) - it("should include changed 'deleted' field", function* () { - // True to false - var item = new Zotero.Item('book'); - item.deleted = true; - var id = yield item.saveTx(); - item = yield Zotero.Items.getAsync(id); - var patchBase = item.toJSON(); - - item.deleted = false; - var json = item.toJSON({ - patchBase: patchBase - }) - assert.isUndefined(json.title); - assert.isFalse(json.deleted); - - // False to true - var item = new Zotero.Item('book'); - item.deleted = false; - var id = yield item.saveTx(); - item = yield Zotero.Items.getAsync(id); - var patchBase = item.toJSON(); - - item.deleted = true; - var json = item.toJSON({ - patchBase: patchBase - }) - assert.isUndefined(json.title); - assert.strictEqual(json.deleted, 1); - }) - it("should set 'parentItem' to false when cleared", function* () { var item = yield createDataObject('item'); var note = new Zotero.Item('note'); @@ -1810,19 +1744,6 @@ describe("Zotero.Item", function () { assert.lengthOf(item.getNotes(), 0); }); - it("should remove item from trash if 'deleted' property not provided", async function () { - var item = await createDataObject('item', { deleted: true }); - - assert.isTrue(item.deleted); - - var json = item.toJSON(); - delete json.deleted; - - item.fromJSON(json); - await item.saveTx(); - - assert.isFalse(item.deleted); - }); it("should remove item from My Publications if 'inPublications' property not provided", async function () { var item = await createDataObject('item', { inPublications: true }); diff --git a/test/tests/searchTest.js b/test/tests/searchTest.js index 3f34722dd4..3d550364e3 100644 --- a/test/tests/searchTest.js +++ b/test/tests/searchTest.js @@ -498,6 +498,19 @@ describe("Zotero.Search", function() { }); }); + describe("#deleted", function () { + it("should set trash status", async function () { + var search = await createDataObject('search'); + assert.isFalse(search.deleted); + search.deleted = true; + await search.saveTx(); + assert.isTrue(search.deleted); + search.deleted = false; + await search.saveTx(); + assert.isFalse(search.deleted); + }); + }); + describe("#toJSON()", function () { it("should output all data", function* () { let s = new Zotero.Search();