Add Retracted Items virtual collection
Shown automatically when retracted items are detected
This commit is contained in:
parent
502f5fe491
commit
5c03813d81
16 changed files with 417 additions and 78 deletions
|
@ -54,6 +54,9 @@ Zotero.CollectionTreeRow.prototype.__defineGetter__('id', function () {
|
|||
case 'unfiled':
|
||||
return 'U' + this.ref.libraryID;
|
||||
|
||||
case 'retracted':
|
||||
return 'R' + this.ref.libraryID;
|
||||
|
||||
case 'publications':
|
||||
return 'P' + this.ref.libraryID;
|
||||
|
||||
|
@ -100,6 +103,10 @@ Zotero.CollectionTreeRow.prototype.isUnfiled = function () {
|
|||
return this.type == 'unfiled';
|
||||
}
|
||||
|
||||
Zotero.CollectionTreeRow.prototype.isRetracted = function () {
|
||||
return this.type == 'retracted';
|
||||
}
|
||||
|
||||
Zotero.CollectionTreeRow.prototype.isTrash = function()
|
||||
{
|
||||
return this.type == 'trash';
|
||||
|
@ -162,7 +169,7 @@ Zotero.CollectionTreeRow.prototype.__defineGetter__('editable', function () {
|
|||
return true;
|
||||
}
|
||||
var libraryID = this.ref.libraryID;
|
||||
if (this.isCollection() || this.isSearch() || this.isDuplicates() || this.isUnfiled()) {
|
||||
if (this.isCollection() || this.isSearch() || this.isDuplicates() || this.isUnfiled() || this.isRetracted()) {
|
||||
var type = Zotero.Libraries.get(libraryID).libraryType;
|
||||
if (type == 'group') {
|
||||
var groupID = Zotero.Groups.getGroupIDFromLibraryID(libraryID);
|
||||
|
@ -185,7 +192,7 @@ Zotero.CollectionTreeRow.prototype.__defineGetter__('filesEditable', function ()
|
|||
if (this.isGroup()) {
|
||||
return this.ref.filesEditable;
|
||||
}
|
||||
if (this.isCollection() || this.isSearch() || this.isDuplicates() || this.isUnfiled()) {
|
||||
if (this.isCollection() || this.isSearch() || this.isDuplicates() || this.isUnfiled() || this.isRetracted()) {
|
||||
var type = Zotero.Libraries.get(libraryID).libraryType;
|
||||
if (type == 'group') {
|
||||
var groupID = Zotero.Groups.getGroupIDFromLibraryID(libraryID);
|
||||
|
|
|
@ -183,6 +183,8 @@ Zotero.CollectionTreeView.prototype.refresh = Zotero.Promise.coroutine(function*
|
|||
}
|
||||
this._virtualCollectionLibraries.unfiled =
|
||||
Zotero.Utilities.Internal.getVirtualCollectionState('unfiled')
|
||||
this._virtualCollectionLibraries.retracted =
|
||||
Zotero.Utilities.Internal.getVirtualCollectionState('retracted');
|
||||
|
||||
var oldCount = this.rowCount || 0;
|
||||
var newRows = [];
|
||||
|
@ -790,6 +792,9 @@ Zotero.CollectionTreeView.prototype.getImageSrc = function(row, col)
|
|||
|
||||
case 'publications':
|
||||
return "chrome://zotero/skin/treeitem-journalArticle" + suffix + ".png";
|
||||
|
||||
case 'retracted':
|
||||
return "chrome://zotero/skin/cross" + suffix + ".png";
|
||||
}
|
||||
|
||||
return "chrome://zotero/skin/treesource-" + collectionType + suffix + ".png";
|
||||
|
@ -823,6 +828,8 @@ Zotero.CollectionTreeView.prototype.isContainerEmpty = function(row)
|
|||
|| this._virtualCollectionLibraries.duplicates[libraryID] === false)
|
||||
// Unfiled Items not shown
|
||||
&& this._virtualCollectionLibraries.unfiled[libraryID] === false
|
||||
// Retracted Items not shown
|
||||
&& this._virtualCollectionLibraries.retracted[libraryID] === false
|
||||
&& this.hideSources.indexOf('trash') != -1;
|
||||
}
|
||||
if (treeRow.isCollection()) {
|
||||
|
@ -1071,6 +1078,7 @@ Zotero.CollectionTreeView.prototype.selectByID = Zotero.Promise.coroutine(functi
|
|||
|
||||
case 'D':
|
||||
case 'U':
|
||||
case 'R':
|
||||
yield this.expandLibrary(id);
|
||||
break;
|
||||
|
||||
|
@ -1326,6 +1334,8 @@ Zotero.CollectionTreeView.prototype._expandRow = Zotero.Promise.coroutine(functi
|
|||
var showDuplicates = this.hideSources.indexOf('duplicates') == -1
|
||||
&& this._virtualCollectionLibraries.duplicates[libraryID] !== false;
|
||||
var showUnfiled = this._virtualCollectionLibraries.unfiled[libraryID] !== false;
|
||||
var showRetracted = this._virtualCollectionLibraries.retracted[libraryID] !== false
|
||||
&& Zotero.Retractions.libraryHasRetractedItems(libraryID);
|
||||
var showPublications = libraryID == Zotero.Libraries.userLibraryID;
|
||||
var showTrash = this.hideSources.indexOf('trash') == -1;
|
||||
}
|
||||
|
@ -1333,6 +1343,7 @@ Zotero.CollectionTreeView.prototype._expandRow = Zotero.Promise.coroutine(functi
|
|||
var savedSearches = [];
|
||||
var showDuplicates = false;
|
||||
var showUnfiled = false;
|
||||
var showRetracted = false;
|
||||
var showPublications = false;
|
||||
var showTrash = false;
|
||||
}
|
||||
|
@ -1346,7 +1357,7 @@ Zotero.CollectionTreeView.prototype._expandRow = Zotero.Promise.coroutine(functi
|
|||
return 0;
|
||||
}
|
||||
|
||||
var startOpen = !!(collections.length || savedSearches.length || showDuplicates || showUnfiled || showTrash);
|
||||
var startOpen = !!(collections.length || savedSearches.length || showDuplicates || showUnfiled || showRetracted || showTrash);
|
||||
|
||||
// If this isn't a manual open, set the initial state depending on whether
|
||||
// there are child nodes
|
||||
|
@ -1430,6 +1441,21 @@ Zotero.CollectionTreeView.prototype._expandRow = Zotero.Promise.coroutine(functi
|
|||
newRows++;
|
||||
}
|
||||
|
||||
// Retracted items
|
||||
if (showRetracted) {
|
||||
let s = new Zotero.Search;
|
||||
s.libraryID = libraryID;
|
||||
s.name = Zotero.getString('pane.collections.retracted');
|
||||
s.addCondition('libraryID', 'is', libraryID);
|
||||
s.addCondition('retracted', 'true');
|
||||
this._addRowToArray(
|
||||
rows,
|
||||
new Zotero.CollectionTreeRow(this, 'retracted', s, level + 1),
|
||||
row + 1 + newRows
|
||||
);
|
||||
newRows++;
|
||||
}
|
||||
|
||||
if (showTrash) {
|
||||
let deletedItems = yield Zotero.Items.getDeleted(libraryID);
|
||||
if (deletedItems.length || Zotero.Prefs.get("showTrashWhenEmpty")) {
|
||||
|
|
|
@ -972,6 +972,10 @@ Zotero.Search.prototype._buildQuery = Zotero.Promise.coroutine(function* () {
|
|||
var unfiled = condition.operator == 'true';
|
||||
continue;
|
||||
|
||||
case 'retracted':
|
||||
var retracted = condition.operator == 'true';
|
||||
continue;
|
||||
|
||||
case 'publications':
|
||||
var publications = condition.operator == 'true';
|
||||
continue;
|
||||
|
@ -1034,6 +1038,10 @@ Zotero.Search.prototype._buildQuery = Zotero.Promise.coroutine(function* () {
|
|||
+ "AND itemID NOT IN (SELECT itemID FROM publicationsItems)";
|
||||
}
|
||||
|
||||
if (retracted) {
|
||||
sql += " AND (itemID IN (SELECT itemID FROM retractedItems))";
|
||||
}
|
||||
|
||||
if (publications) {
|
||||
sql += " AND (itemID IN (SELECT itemID FROM publicationsItems))";
|
||||
}
|
||||
|
|
|
@ -99,6 +99,14 @@ Zotero.SearchConditions = new function(){
|
|||
}
|
||||
},
|
||||
|
||||
{
|
||||
name: 'retracted',
|
||||
operators: {
|
||||
true: true,
|
||||
false: true
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
name: 'publications',
|
||||
operators: {
|
||||
|
|
|
@ -44,13 +44,17 @@ Zotero.Retractions = {
|
|||
_keyItems: {},
|
||||
_itemKeys: {},
|
||||
|
||||
_retractedItems: new Set(),
|
||||
_retractedItemsByLibrary: {},
|
||||
_librariesWithRetractions: new Set(),
|
||||
|
||||
init: async function () {
|
||||
this._cacheFile = OS.Path.join(Zotero.Profile.dir, 'retractions.json');
|
||||
|
||||
// Load mappings of keys (DOI hashes and PMIDs) to items and vice versa and register for
|
||||
// item changes so they can be kept up to date in notify().
|
||||
await this._cacheKeyMappings();
|
||||
Zotero.Notifier.registerObserver(this, ['item'], 'retractions');
|
||||
Zotero.Notifier.registerObserver(this, ['item', 'group'], 'retractions', 20);
|
||||
|
||||
// Load in the cached prefix list that we check new items against
|
||||
try {
|
||||
|
@ -62,8 +66,20 @@ Zotero.Retractions = {
|
|||
}
|
||||
|
||||
// Load existing retracted items
|
||||
var itemIDs = await Zotero.DB.columnQueryAsync("SELECT itemID FROM retractedItems");
|
||||
this._retractedItems = new Set(itemIDs);
|
||||
var rows = await Zotero.DB.queryAsync(
|
||||
"SELECT libraryID, itemID, DI.itemID IS NOT NULL AS deleted FROM items "
|
||||
+ "JOIN retractedItems USING (itemID) "
|
||||
+ "LEFT JOIN deletedItems DI USING (itemID)"
|
||||
);
|
||||
for (let row of rows) {
|
||||
this._retractedItems.add(row.itemID);
|
||||
if (!row.deleted) {
|
||||
if (!this._retractedItemsByLibrary[row.libraryID]) {
|
||||
this._retractedItemsByLibrary[row.libraryID] = new Set();
|
||||
}
|
||||
this._retractedItemsByLibrary[row.libraryID].add(row.itemID);
|
||||
}
|
||||
}
|
||||
|
||||
this._initialized = true;
|
||||
|
||||
|
@ -81,6 +97,55 @@ Zotero.Retractions = {
|
|||
return this._retractedItems.has(item.id);
|
||||
},
|
||||
|
||||
libraryHasRetractedItems: function (libraryID) {
|
||||
return !!(this._retractedItemsByLibrary[libraryID]
|
||||
&& this._retractedItemsByLibrary[libraryID].size);
|
||||
},
|
||||
|
||||
_addLibraryRetractedItem: async function (libraryID, itemID) {
|
||||
if (!this._retractedItemsByLibrary[libraryID]) {
|
||||
this._retractedItemsByLibrary[libraryID] = new Set();
|
||||
}
|
||||
this._retractedItemsByLibrary[libraryID].add(itemID);
|
||||
await this._updateLibraryRetractions(libraryID);
|
||||
},
|
||||
|
||||
_removeLibraryRetractedItem: async function (libraryID, itemID) {
|
||||
this._retractedItemsByLibrary[libraryID].delete(itemID);
|
||||
await this._updateLibraryRetractions(libraryID);
|
||||
},
|
||||
|
||||
_updateLibraryRetractions: async function (libraryID) {
|
||||
var previous = this._librariesWithRetractions.has(libraryID);
|
||||
var current = this.libraryHasRetractedItems(libraryID);
|
||||
|
||||
// Update Retracted Items virtual collection
|
||||
if (Zotero.Libraries.exists(libraryID)
|
||||
// Changed
|
||||
&& (previous != current ||
|
||||
// Explicitly hidden
|
||||
(current && !Zotero.Utilities.Internal.getVirtualCollectionStateForLibrary(libraryID, 'retracted')))) {
|
||||
let promises = [];
|
||||
for (let zp of Zotero.getZoteroPanes()) {
|
||||
promises.push(zp.setVirtual(libraryID, 'retracted', current));
|
||||
zp.hideRetractionBanner();
|
||||
}
|
||||
await Zotero.Promise.all(promises);
|
||||
}
|
||||
|
||||
if (current) {
|
||||
this._librariesWithRetractions.add(libraryID);
|
||||
}
|
||||
else {
|
||||
this._librariesWithRetractions.delete(libraryID);
|
||||
}
|
||||
},
|
||||
|
||||
_resetLibraryRetractions: function (libraryID) {
|
||||
delete this._retractedItemsByLibrary[libraryID];
|
||||
this._updateLibraryRetractions(libraryID);
|
||||
},
|
||||
|
||||
/**
|
||||
* Return retraction data for an item
|
||||
*
|
||||
|
@ -127,7 +192,16 @@ Zotero.Retractions = {
|
|||
return description;
|
||||
},
|
||||
|
||||
notify: async function (action, type, ids, _extraData) {
|
||||
notify: async function (action, type, ids, extraData) {
|
||||
// Clean up cache on group deletion
|
||||
if (action == 'delete' && type == 'group') {
|
||||
for (let libraryID of ids) {
|
||||
this._resetLibraryRetractions(libraryID);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Items
|
||||
if (action == 'add') {
|
||||
for (let id of ids) {
|
||||
this._updateItem(Zotero.Items.get(id));
|
||||
|
@ -140,7 +214,7 @@ Zotero.Retractions = {
|
|||
let typeID = this['TYPE_' + type];
|
||||
let fieldVal = this['_getItem' + type](item);
|
||||
if (fieldVal) {
|
||||
// If the item isn't already mapped to the key, re-map
|
||||
// If the item isn't already mapped to the key, re-map and re-check
|
||||
let key = this._itemKeys[typeID].get(item.id);
|
||||
let newKey = this._valueToKey(typeID, fieldVal);
|
||||
if (key != newKey) {
|
||||
|
@ -149,18 +223,31 @@ Zotero.Retractions = {
|
|||
continue;
|
||||
}
|
||||
}
|
||||
// If a previous key value was cleared, re-map
|
||||
// If a previous key value was cleared, re-map and re-check
|
||||
else if (this._itemKeys[typeID].get(item.id)) {
|
||||
this._deleteItemKeyMappings(id);
|
||||
this._updateItem(item);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// We don't want to show the virtual collection for items in the trash, so add or
|
||||
// remove from the library set depending on whether it's in the trash. This is
|
||||
// handled for newly detected items in _addEntry(), which gets called by
|
||||
// _updateItem() above after a delay (such that the item won't yet be retracted
|
||||
// here).
|
||||
if (this._retractedItems.has(item.id)) {
|
||||
if (item.deleted) {
|
||||
await this._removeLibraryRetractedItem(item.libraryID, item.id);
|
||||
}
|
||||
else {
|
||||
await this._addLibraryRetractedItem(item.libraryID, item.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (action == 'delete') {
|
||||
for (let id of ids) {
|
||||
await this._removeEntry(id);
|
||||
await this._removeEntry(id, extraData[id].libraryID);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -169,12 +256,16 @@ Zotero.Retractions = {
|
|||
* Check for possible matches for items in the queue (debounced)
|
||||
*/
|
||||
checkQueuedItems: Zotero.Utilities.debounce(async function () {
|
||||
return this._checkQueuedItemsInternal();
|
||||
}, 1000),
|
||||
|
||||
_checkQueuedItemsInternal: async function () {
|
||||
Zotero.debug("Checking updated items for retractions");
|
||||
|
||||
// If no possible matches, clear retraction flag on any items that changed
|
||||
if (!this._queuedPrefixStrings.size) {
|
||||
for (let item of this._queuedItems) {
|
||||
await this._removeEntry(item.id);
|
||||
await this._removeEntry(item.id, item.libraryID);
|
||||
}
|
||||
this._queuedItems.clear();
|
||||
return;
|
||||
|
@ -202,10 +293,10 @@ Zotero.Retractions = {
|
|||
// Remove retraction status for items that were checked but didn't match
|
||||
for (let item of items) {
|
||||
if (!addedItems.includes(item.id)) {
|
||||
await this._removeEntry(item.id);
|
||||
await this._removeEntry(item.id, item.libraryID);
|
||||
}
|
||||
}
|
||||
}, 1000),
|
||||
},
|
||||
|
||||
updateFromServer: Zotero.serial(async function () {
|
||||
if (!this._initialized) {
|
||||
|
@ -231,7 +322,12 @@ Zotero.Retractions = {
|
|||
return;
|
||||
}
|
||||
var etag = req.getResponseHeader('ETag');
|
||||
var list = req.response.trim().split('\n');
|
||||
var list = req.response.split('\n').filter(x => x);
|
||||
|
||||
if (!list.length) {
|
||||
Zotero.logError("Empty retraction list from server");
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate prefix length automatically
|
||||
var doiPrefixLength;
|
||||
|
@ -269,17 +365,15 @@ Zotero.Retractions = {
|
|||
}
|
||||
}
|
||||
|
||||
if (!prefixesToSend.size) {
|
||||
if (prefixesToSend.size) {
|
||||
// TODO: Diff list and remove existing retractions that are missing
|
||||
|
||||
await this._downloadPossibleMatches([...prefixesToSend]);
|
||||
}
|
||||
else {
|
||||
Zotero.debug("No possible retractions");
|
||||
await Zotero.DB.queryAsync("DELETE FROM retractedItems");
|
||||
this._retractedItems.clear();
|
||||
await this._saveCacheFile(list, etag, doiPrefixLength, pmidPrefixLength);
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Diff list
|
||||
|
||||
await this._downloadPossibleMatches([...prefixesToSend]);
|
||||
await this._saveCacheFile(list, etag, doiPrefixLength, pmidPrefixLength);
|
||||
}),
|
||||
|
||||
|
@ -492,7 +586,7 @@ Zotero.Retractions = {
|
|||
return;
|
||||
}
|
||||
this._queuedItems.add(item);
|
||||
var doi = this._getItemDOI(item);
|
||||
let doi = this._getItemDOI(item);
|
||||
if (doi) {
|
||||
this._addItemKeyMapping(this.TYPE_DOI, doi, item.id);
|
||||
let prefixStr = this.TYPE_DOI + this._getDOIPrefix(doi, this._cacheDOIPrefixLength);
|
||||
|
@ -500,7 +594,7 @@ Zotero.Retractions = {
|
|||
this._queuedPrefixStrings.add(prefixStr);
|
||||
}
|
||||
}
|
||||
var pmid = this._getItemPMID(item);
|
||||
let pmid = this._getItemPMID(item);
|
||||
if (pmid) {
|
||||
this._addItemKeyMapping(this.TYPE_PMID, pmid, item.id);
|
||||
let prefixStr = this.TYPE_PMID + this._getPMIDPrefix(pmid, this._cachePMIDPrefixLength);
|
||||
|
@ -523,19 +617,31 @@ Zotero.Retractions = {
|
|||
var sql = "REPLACE INTO retractedItems VALUES (?, ?)";
|
||||
await Zotero.DB.queryAsync(sql, [itemID, JSON.stringify(o)]);
|
||||
|
||||
var item = Zotero.Items.get(itemID);
|
||||
var libraryID = item.libraryID;
|
||||
this._retractedItems.add(itemID);
|
||||
if (!item.deleted) {
|
||||
if (!this._retractedItemsByLibrary[libraryID]) {
|
||||
this._retractedItemsByLibrary[libraryID] = new Set();
|
||||
}
|
||||
this._retractedItemsByLibrary[libraryID].add(itemID);
|
||||
await this._updateLibraryRetractions(libraryID);
|
||||
}
|
||||
|
||||
await Zotero.Notifier.trigger('refresh', 'item', [itemID]);
|
||||
},
|
||||
|
||||
_removeEntry: async function (itemID) {
|
||||
_removeEntry: async function (itemID, libraryID) {
|
||||
this._deleteItemKeyMappings(itemID);
|
||||
|
||||
if (this._retractedItems.has(itemID)) {
|
||||
await Zotero.DB.queryAsync("DELETE FROM retractedItems WHERE itemID=?", itemID);
|
||||
if (!this._retractedItems.has(itemID)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await Zotero.DB.queryAsync("DELETE FROM retractedItems WHERE itemID=?", itemID);
|
||||
this._retractedItems.delete(itemID);
|
||||
this._retractedItemsByLibrary[libraryID].delete(itemID);
|
||||
await this._updateLibraryRetractions(libraryID);
|
||||
|
||||
await Zotero.Notifier.trigger('refresh', 'item', [itemID]);
|
||||
},
|
||||
|
|
|
@ -1410,6 +1410,10 @@ Zotero.Utilities.Internal = {
|
|||
var prefKey = 'unfiledLibraries';
|
||||
break;
|
||||
|
||||
case 'retracted':
|
||||
var prefKey = 'retractedLibraries';
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error("Invalid virtual collection type '" + type + "'");
|
||||
}
|
||||
|
@ -1445,6 +1449,10 @@ Zotero.Utilities.Internal = {
|
|||
var prefKey = 'unfiledLibraries';
|
||||
break;
|
||||
|
||||
case 'retracted':
|
||||
var prefKey = 'retractedLibraries';
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error("Invalid virtual collection type '" + type + "'");
|
||||
}
|
||||
|
|
|
@ -71,6 +71,17 @@ Services.scriptloader.loadSubScript("resource://zotero/polyfill.js");
|
|||
return win ? win.ZoteroPane : null;
|
||||
};
|
||||
|
||||
this.getZoteroPanes = function () {
|
||||
var enumerator = Services.wm.getEnumerator("navigator:browser");
|
||||
var zps = [];
|
||||
while (enumerator.hasMoreElements()) {
|
||||
let win = enumerator.getNext();
|
||||
if (!win.ZoteroPane) continue;
|
||||
zps.push(win.ZoteroPane);
|
||||
}
|
||||
return zps;
|
||||
};
|
||||
|
||||
/**
|
||||
* @property {Boolean} locked Whether all Zotero panes are locked
|
||||
* with an overlay
|
||||
|
|
|
@ -1027,7 +1027,7 @@ var ZoteroPane = new function()
|
|||
});
|
||||
|
||||
|
||||
this.setVirtual = Zotero.Promise.coroutine(function* (libraryID, type, show) {
|
||||
this.setVirtual = Zotero.Promise.coroutine(function* (libraryID, type, show, select) {
|
||||
switch (type) {
|
||||
case 'duplicates':
|
||||
var treeViewID = 'D' + libraryID;
|
||||
|
@ -1037,6 +1037,10 @@ var ZoteroPane = new function()
|
|||
var treeViewID = 'U' + libraryID;
|
||||
break;
|
||||
|
||||
case 'retracted':
|
||||
var treeViewID = 'R' + libraryID;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error("Invalid virtual collection type '" + type + "'");
|
||||
}
|
||||
|
@ -1046,13 +1050,17 @@ var ZoteroPane = new function()
|
|||
var cv = this.collectionsView;
|
||||
|
||||
var promise = cv.waitForSelect();
|
||||
var selectedRowID = cv.selectedTreeRow.id;
|
||||
var selectedRow = cv.selection.currentIndex;
|
||||
|
||||
yield cv.refresh();
|
||||
|
||||
// Select new row
|
||||
// Select new or original row
|
||||
if (show) {
|
||||
yield this.collectionsView.selectByID(treeViewID);
|
||||
yield this.collectionsView.selectByID(select ? treeViewID : selectedRowID);
|
||||
}
|
||||
else if (type == 'retracted') {
|
||||
yield this.collectionsView.selectByID("L" + libraryID);
|
||||
}
|
||||
// Select next appropriate row after removal
|
||||
else {
|
||||
|
@ -1791,7 +1799,10 @@ var ZoteroPane = new function()
|
|||
|
||||
var prompt = force ? toTrash : toRemove;
|
||||
}
|
||||
else if (collectionTreeRow.isSearch() || collectionTreeRow.isUnfiled() || collectionTreeRow.isDuplicates()) {
|
||||
else if (collectionTreeRow.isSearch()
|
||||
|| collectionTreeRow.isUnfiled()
|
||||
|| collectionTreeRow.isRetracted()
|
||||
|| collectionTreeRow.isDuplicates()) {
|
||||
if (!force) {
|
||||
return;
|
||||
}
|
||||
|
@ -1865,6 +1876,11 @@ var ZoteroPane = new function()
|
|||
this.setVirtual(collectionTreeRow.ref.libraryID, 'unfiled', false);
|
||||
return;
|
||||
}
|
||||
// Remove virtual retracted collection
|
||||
else if (collectionTreeRow.isRetracted()) {
|
||||
this.setVirtual(collectionTreeRow.ref.libraryID, 'retracted', false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.canEdit() && !collectionTreeRow.isFeed()) {
|
||||
this.displayCannotEditLibraryMessage();
|
||||
|
@ -2401,13 +2417,19 @@ var ZoteroPane = new function()
|
|||
{
|
||||
id: "showDuplicates",
|
||||
oncommand: () => {
|
||||
this.setVirtual(this.getSelectedLibraryID(), 'duplicates', true);
|
||||
this.setVirtual(this.getSelectedLibraryID(), 'duplicates', true, true);
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "showUnfiled",
|
||||
oncommand: () => {
|
||||
this.setVirtual(this.getSelectedLibraryID(), 'unfiled', true);
|
||||
this.setVirtual(this.getSelectedLibraryID(), 'unfiled', true, true);
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "showRetracted",
|
||||
oncommand: () => {
|
||||
this.setVirtual(this.getSelectedLibraryID(), 'retracted', true, true);
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -2600,7 +2622,7 @@ var ZoteroPane = new function()
|
|||
else if (collectionTreeRow.isTrash()) {
|
||||
show = ['emptyTrash'];
|
||||
}
|
||||
else if (collectionTreeRow.isDuplicates() || collectionTreeRow.isUnfiled()) {
|
||||
else if (collectionTreeRow.isDuplicates() || collectionTreeRow.isUnfiled() || collectionTreeRow.isRetracted()) {
|
||||
show = ['deleteCollection'];
|
||||
|
||||
m.deleteCollection.setAttribute('label', Zotero.getString('general.hide'));
|
||||
|
@ -2624,14 +2646,17 @@ var ZoteroPane = new function()
|
|||
'newSavedSearch'
|
||||
);
|
||||
}
|
||||
// Only show "Show Duplicates" and "Show Unfiled Items" if rows are hidden
|
||||
// Only show "Show Duplicates", "Show Unfiled Items", and "Show Retracted" if rows are hidden
|
||||
let duplicates = Zotero.Utilities.Internal.getVirtualCollectionStateForLibrary(
|
||||
libraryID, 'duplicates'
|
||||
);
|
||||
let unfiled = Zotero.Utilities.Internal.getVirtualCollectionStateForLibrary(
|
||||
libraryID, 'unfiled'
|
||||
);
|
||||
if (!duplicates || !unfiled) {
|
||||
let retracted = Zotero.Utilities.Internal.getVirtualCollectionStateForLibrary(
|
||||
libraryID, 'retracted'
|
||||
);
|
||||
if (!duplicates || !unfiled || !retracted) {
|
||||
if (!library.archived) {
|
||||
show.push('sep2');
|
||||
}
|
||||
|
@ -2641,6 +2666,9 @@ var ZoteroPane = new function()
|
|||
if (!unfiled) {
|
||||
show.push('showUnfiled');
|
||||
}
|
||||
if (!retracted) {
|
||||
show.push('showRetracted');
|
||||
}
|
||||
}
|
||||
if (!library.archived) {
|
||||
show.push('sep3');
|
||||
|
@ -2656,7 +2684,11 @@ var ZoteroPane = new function()
|
|||
// Disable some actions if user doesn't have write access
|
||||
//
|
||||
// Some actions are disabled via their commands in onCollectionSelected()
|
||||
if (collectionTreeRow.isWithinGroup() && !collectionTreeRow.editable && !collectionTreeRow.isDuplicates() && !collectionTreeRow.isUnfiled()) {
|
||||
if (collectionTreeRow.isWithinGroup()
|
||||
&& !collectionTreeRow.editable
|
||||
&& !collectionTreeRow.isDuplicates()
|
||||
&& !collectionTreeRow.isUnfiled()
|
||||
&& !collectionTreeRow.isRetracted()) {
|
||||
disable.push(
|
||||
'newSubcollection',
|
||||
'editSelectedCollection',
|
||||
|
@ -3219,8 +3251,8 @@ var ZoteroPane = new function()
|
|||
return;
|
||||
}
|
||||
|
||||
// Ignore double-clicks on Unfiled Items source row
|
||||
if (collectionTreeRow.isUnfiled()) {
|
||||
// Ignore double-clicks on Unfiled/Retracted Items source rows
|
||||
if (collectionTreeRow.isUnfiled() || collectionTreeRow.isRetracted()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -4762,12 +4794,15 @@ var ZoteroPane = new function()
|
|||
var suffix = items.length > 1 ? 'multiple' : 'single';
|
||||
message.textContent = Zotero.getString('retraction.alert.' + suffix);
|
||||
link.textContent = Zotero.getString('retraction.alert.view.' + suffix);
|
||||
link.onclick = function () {
|
||||
var libraryID = this.getSelectedLibraryID();
|
||||
// Pick the first item we find in the current library, or just pick one at random
|
||||
var item = items.find(item => item.libraryID == libraryID) || items[0];
|
||||
this.selectItem(item.id);
|
||||
link.onclick = async function () {
|
||||
this.hideRetractionBanner();
|
||||
var libraryID = this.getSelectedLibraryID();
|
||||
// Select the Retracted Items collection
|
||||
await this.collectionsView.selectByID("R" + libraryID);
|
||||
// Select newly detected item if only one
|
||||
if (items.length == 1) {
|
||||
await this.selectItem(items[0].id);
|
||||
}
|
||||
}.bind(this);
|
||||
|
||||
close.onclick = function () {
|
||||
|
|
|
@ -216,7 +216,7 @@
|
|||
</toolbar>
|
||||
|
||||
<vbox id="retracted-items-container" collapsed="true">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" id="retracted-items-banner" collapsed="true">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" id="retracted-items-banner">
|
||||
<div id="retracted-items-message"/>
|
||||
<div id="retracted-items-link"/>
|
||||
<div id="retracted-items-close">×</div>
|
||||
|
@ -236,6 +236,7 @@
|
|||
<menuseparator/>
|
||||
<menuitem class="zotero-menuitem-show-duplicates" label="&zotero.toolbar.duplicate.label;"/>
|
||||
<menuitem class="zotero-menuitem-show-unfiled" label="&zotero.collections.showUnfiledItems;"/>
|
||||
<menuitem class="zotero-menuitem-show-retracted" label="&zotero.collections.showRetractedItems;"/>
|
||||
<menuitem class="zotero-menuitem-edit-collection"/>
|
||||
<menuitem class="zotero-menuitem-duplicate-collection"/>
|
||||
<menuitem class="zotero-menuitem-mark-read-feed" label="&zotero.toolbar.markFeedRead.label;"/>
|
||||
|
|
|
@ -50,6 +50,7 @@
|
|||
|
||||
<!ENTITY zotero.toolbar.duplicate.label "Show Duplicates">
|
||||
<!ENTITY zotero.collections.showUnfiledItems "Show Unfiled Items">
|
||||
<!ENTITY zotero.collections.showRetractedItems "Show Retracted Items">
|
||||
|
||||
<!ENTITY zotero.items.itemType "Item Type">
|
||||
<!ENTITY zotero.items.type_column "Item Type">
|
||||
|
|
|
@ -234,6 +234,7 @@ pane.collections.feedLibraries = Feeds
|
|||
pane.collections.trash = Trash
|
||||
pane.collections.untitled = Untitled
|
||||
pane.collections.unfiled = Unfiled Items
|
||||
pane.collections.retracted = Retracted Items
|
||||
pane.collections.duplicate = Duplicate Items
|
||||
pane.collections.removeLibrary = Remove Library
|
||||
pane.collections.removeLibrary.text = Are you sure you want to permanently remove “%S” from this computer?
|
||||
|
|
|
@ -445,6 +445,10 @@
|
|||
list-style-image: url('chrome://zotero/skin/treesource-unfiled.png');
|
||||
}
|
||||
|
||||
.zotero-menuitem-show-retracted {
|
||||
list-style-image: url('chrome://zotero/skin/cross.png');
|
||||
}
|
||||
|
||||
.zotero-menuitem-sync {
|
||||
list-style-image: url(chrome://zotero/skin/arrow_rotate_static.png);
|
||||
}
|
||||
|
@ -764,6 +768,7 @@
|
|||
font-size: 22px;
|
||||
}
|
||||
|
||||
|
||||
/* BEGIN 2X BLOCK -- DO NOT EDIT MANUALLY -- USE 2XIZE */
|
||||
@media (min-resolution: 1.25dppx) {
|
||||
#zotero-items-column-hasAttachment { list-style-image: url(chrome://zotero/skin/attach-small@2x.png); }
|
||||
|
@ -780,6 +785,7 @@
|
|||
.zotero-menuitem-new-saved-search { list-style-image: url('chrome://zotero/skin/treesource-search@2x.png'); }
|
||||
.zotero-menuitem-show-duplicates { list-style-image: url('chrome://zotero/skin/treesource-duplicates@2x.png'); }
|
||||
.zotero-menuitem-show-unfiled { list-style-image: url('chrome://zotero/skin/treesource-unfiled@2x.png'); }
|
||||
.zotero-menuitem-show-retracted { list-style-image: url('chrome://zotero/skin/cross@2x.png'); }
|
||||
.zotero-menuitem-sync { list-style-image: url(chrome://zotero/skin/arrow_rotate_static@2x.png); }
|
||||
.zotero-menuitem-new-collection { list-style-image: url('chrome://zotero/skin/toolbar-collection-add@2x.png'); }
|
||||
.zotero-menuitem-edit-collection { list-style-image: url('chrome://zotero/skin/toolbar-collection-edit@2x.png'); }
|
||||
|
|
|
@ -18,20 +18,24 @@ describe("Zotero.CollectionTreeView", function() {
|
|||
});
|
||||
|
||||
describe("#refresh()", function () {
|
||||
it("should show Duplicate Items and Unfiled Items by default", function* () {
|
||||
it("should show Duplicate Items and Unfiled Items by default and shouldn't show Retracted Items", function* () {
|
||||
Zotero.Prefs.clear('duplicateLibraries');
|
||||
Zotero.Prefs.clear('unfiledLibraries');
|
||||
Zotero.Prefs.clear('retractedLibraries');
|
||||
yield cv.refresh();
|
||||
assert.ok(cv.getRowIndexByID("D" + userLibraryID));
|
||||
assert.ok(cv.getRowIndexByID("U" + userLibraryID));
|
||||
assert.isFalse(cv.getRowIndexByID("R" + userLibraryID));
|
||||
});
|
||||
|
||||
it("shouldn't show Duplicate Items and Unfiled Items if hidden", function* () {
|
||||
it("shouldn't show virtual collections if hidden", function* () {
|
||||
Zotero.Prefs.set('duplicateLibraries', `{"${userLibraryID}": false}`);
|
||||
Zotero.Prefs.set('unfiledLibraries', `{"${userLibraryID}": false}`);
|
||||
Zotero.Prefs.set('retractedLibraries', `{"${userLibraryID}": false}`);
|
||||
yield cv.refresh();
|
||||
assert.isFalse(cv.getRowIndexByID("D" + userLibraryID));
|
||||
assert.isFalse(cv.getRowIndexByID("U" + userLibraryID));
|
||||
assert.isFalse(cv.getRowIndexByID("R" + userLibraryID));
|
||||
});
|
||||
|
||||
it("should maintain open state of group", function* () {
|
||||
|
|
|
@ -595,9 +595,8 @@ describe("Zotero.ItemTreeView", 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 zp.setVirtual(userLibraryID, 'unfiled', true, true);
|
||||
assert.equal(cv.selectedTreeRow.id, 'U' + userLibraryID);
|
||||
yield waitForItemsLoad(win);
|
||||
assert.isNumber(zp.itemsView.getRowIndexByID(item.id));
|
||||
yield Zotero.DB.executeTransaction(function* () {
|
||||
|
|
|
@ -1,34 +1,93 @@
|
|||
describe("Retractions", function() {
|
||||
var userLibraryID;
|
||||
var win;
|
||||
var zp;
|
||||
var server;
|
||||
var checkQueueItemsStub;
|
||||
var retractedDOI = '10.1016/S0140-6736(97)11096-0';
|
||||
|
||||
before(async function () {
|
||||
userLibraryID = Zotero.Libraries.userLibraryID;
|
||||
win = await loadZoteroPane();
|
||||
zp = win.ZoteroPane;
|
||||
|
||||
// Remove debouncing on checkQueuedItems()
|
||||
checkQueueItemsStub = sinon.stub(Zotero.Retractions, 'checkQueuedItems').callsFake(() => {
|
||||
return Zotero.Retractions._checkQueuedItemsInternal();
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(async function () {
|
||||
var ids = await Zotero.DB.columnQueryAsync("SELECT itemID FROM retractedItems");
|
||||
if (ids.length) {
|
||||
await Zotero.Items.erase(ids);
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(async function () {
|
||||
win.document.getElementById('retracted-items-close').click();
|
||||
checkQueueItemsStub.resetHistory();
|
||||
});
|
||||
|
||||
after(async function () {
|
||||
win.close();
|
||||
checkQueueItemsStub.restore();
|
||||
|
||||
var ids = await Zotero.DB.columnQueryAsync("SELECT itemID FROM retractedItems");
|
||||
if (ids.length) {
|
||||
await Zotero.Items.erase(ids);
|
||||
}
|
||||
});
|
||||
|
||||
async function createRetractedItem(options = {}) {
|
||||
var o = {
|
||||
itemType: 'journalArticle'
|
||||
};
|
||||
Object.assign(o, options);
|
||||
var item = createUnsavedDataObject('item', o);
|
||||
item.setField('DOI', retractedDOI);
|
||||
if (Zotero.DB.inTransaction) {
|
||||
await item.save();
|
||||
}
|
||||
else {
|
||||
await item.saveTx();
|
||||
}
|
||||
|
||||
while (!checkQueueItemsStub.called) {
|
||||
await Zotero.Promise.delay(50);
|
||||
}
|
||||
await checkQueueItemsStub.returnValues[0];
|
||||
checkQueueItemsStub.resetHistory();
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
describe("Notification Banner", function () {
|
||||
var win;
|
||||
var zp;
|
||||
function bannerShown() {
|
||||
var container = win.document.getElementById('retracted-items-container');
|
||||
if (container.getAttribute('collapsed') == 'true') {
|
||||
return false;
|
||||
}
|
||||
if (!container.hasAttribute('collapsed')) {
|
||||
return true;
|
||||
}
|
||||
throw new Error("'collapsed' attribute not found");
|
||||
}
|
||||
|
||||
before(async function () {
|
||||
win = await loadZoteroPane();
|
||||
zp = win.ZoteroPane;
|
||||
it("should show banner when retracted item is added", async function () {
|
||||
var banner = win.document.getElementById('retracted-items-container');
|
||||
assert.isFalse(bannerShown());
|
||||
|
||||
await createRetractedItem();
|
||||
|
||||
assert.isTrue(bannerShown());
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
win.document.getElementById('retracted-items-close').click();
|
||||
});
|
||||
|
||||
after(function () {
|
||||
win.close();
|
||||
});
|
||||
|
||||
it("shouldn't select item in trash", async function () {
|
||||
var item1 = await createDataObject('item', { deleted: true });
|
||||
var item2 = await createDataObject('item');
|
||||
var item3 = await createDataObject('item', { deleted: true });
|
||||
it("shouldn't show banner when item in trash is added", async function () {
|
||||
var item = await createRetractedItem({ deleted: true });
|
||||
|
||||
await Zotero.Retractions._addEntry(item1.id, {});
|
||||
await Zotero.Retractions._addEntry(item2.id, {});
|
||||
await Zotero.Retractions._addEntry(item3.id, {});
|
||||
assert.isFalse(bannerShown());
|
||||
|
||||
await createDataObject('collection');
|
||||
await waitForItemsLoad(win);
|
||||
|
||||
await Zotero.Retractions._showAlert([item1.id, item2.id, item3.id]);
|
||||
win.document.getElementById('retracted-items-link').click();
|
||||
|
||||
while (zp.collectionsView.selectedTreeRow.id != 'L1') {
|
||||
|
@ -37,7 +96,66 @@ describe("Retractions", function() {
|
|||
await waitForItemsLoad(win);
|
||||
|
||||
var item = await zp.getSelectedItems()[0];
|
||||
assert.equal(item, item2);
|
||||
assert.equal(item, item);
|
||||
});
|
||||
});
|
||||
|
||||
describe("virtual collection", function () {
|
||||
it("should show/hide Retracted Items collection when a retracted item is found/erased", async function () {
|
||||
// Create item
|
||||
var item = await createRetractedItem();
|
||||
assert.ok(zp.collectionsView.getRowIndexByID("R" + userLibraryID));
|
||||
|
||||
// Erase item
|
||||
var promise = waitForItemEvent('refresh');
|
||||
await item.eraseTx();
|
||||
await promise;
|
||||
assert.isFalse(zp.collectionsView.getRowIndexByID("R" + userLibraryID));
|
||||
});
|
||||
|
||||
it("should unhide Retracted Items collection when retracted item is found", async function () {
|
||||
await createRetractedItem();
|
||||
|
||||
// Hide collection
|
||||
await zp.setVirtual(userLibraryID, 'retracted', false);
|
||||
|
||||
// Add another retracted item, which should unhide it
|
||||
await createRetractedItem();
|
||||
assert.ok(zp.collectionsView.getRowIndexByID("R" + userLibraryID));
|
||||
});
|
||||
|
||||
it("should hide Retracted Items collection when last retracted item is moved to trash", async function () {
|
||||
var rowID = "R" + userLibraryID;
|
||||
|
||||
// Create item
|
||||
var item = await createRetractedItem();
|
||||
assert.ok(zp.collectionsView.getRowIndexByID(rowID));
|
||||
|
||||
// Select Retracted Items collection
|
||||
await zp.collectionsView.selectByID(rowID);
|
||||
await waitForItemsLoad(win);
|
||||
|
||||
// Erase item
|
||||
item.deleted = true;
|
||||
await item.saveTx();
|
||||
await Zotero.Promise.delay(50);
|
||||
// Retracted Items should be gone
|
||||
assert.isFalse(zp.collectionsView.getRowIndexByID(rowID));
|
||||
// And My Library should be selected
|
||||
assert.equal(zp.collectionsView.selectedTreeRow.id, "L" + userLibraryID);
|
||||
});
|
||||
|
||||
it("should show Retracted Items collection when retracted item is restored from trash", async function () {
|
||||
// Create trashed item
|
||||
var item = await createRetractedItem({ deleted: true });
|
||||
await Zotero.Promise.delay(50);
|
||||
assert.isFalse(zp.collectionsView.getRowIndexByID("R" + userLibraryID));
|
||||
|
||||
// Restore item
|
||||
item.deleted = false;
|
||||
await item.saveTx();
|
||||
await Zotero.Promise.delay(50);
|
||||
assert.ok(zp.collectionsView.getRowIndexByID("R" + userLibraryID));
|
||||
});
|
||||
});
|
||||
});
|
|
@ -470,7 +470,7 @@ describe("ZoteroPane", function() {
|
|||
// Show Duplicate Items
|
||||
var id = "D" + userLibraryID;
|
||||
assert.isFalse(cv.getRowIndexByID(id));
|
||||
yield zp.setVirtual(userLibraryID, 'duplicates', true);
|
||||
yield zp.setVirtual(userLibraryID, 'duplicates', true, true);
|
||||
// Duplicate Items should be selected
|
||||
assert.equal(cv.selectedTreeRow.id, id);
|
||||
// Should be missing from pref
|
||||
|
@ -490,7 +490,7 @@ describe("ZoteroPane", function() {
|
|||
// Show Unfiled Items
|
||||
id = "U" + userLibraryID;
|
||||
assert.isFalse(cv.getRowIndexByID(id));
|
||||
yield zp.setVirtual(userLibraryID, 'unfiled', true);
|
||||
yield zp.setVirtual(userLibraryID, 'unfiled', true, true);
|
||||
// Unfiled Items should be selected
|
||||
assert.equal(cv.selectedTreeRow.id, id);
|
||||
// Should be missing from pref
|
||||
|
@ -510,7 +510,7 @@ describe("ZoteroPane", function() {
|
|||
|
||||
// Show Duplicate Items
|
||||
var id = "D" + userLibraryID;
|
||||
yield zp.setVirtual(userLibraryID, 'duplicates', true);
|
||||
yield zp.setVirtual(userLibraryID, 'duplicates', true, true);
|
||||
|
||||
// Library should have been expanded and Duplicate Items selected
|
||||
assert.ok(cv.getRowIndexByID(id));
|
||||
|
|
Loading…
Reference in a new issue