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.
This commit is contained in:
parent
5a4d78578b
commit
e45ca4edad
16 changed files with 346 additions and 136 deletions
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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.DataObject[]|Integer[]>}
|
||||
*/
|
||||
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]);
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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<Zotero.Item[]|Integer[]>}
|
||||
*/
|
||||
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
|
||||
*
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
})
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue