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':
|
case 'unfiled':
|
||||||
return 'U' + this.ref.libraryID;
|
return 'U' + this.ref.libraryID;
|
||||||
|
|
||||||
|
case 'retracted':
|
||||||
|
return 'R' + this.ref.libraryID;
|
||||||
|
|
||||||
case 'publications':
|
case 'publications':
|
||||||
return 'P' + this.ref.libraryID;
|
return 'P' + this.ref.libraryID;
|
||||||
|
|
||||||
|
@ -100,6 +103,10 @@ Zotero.CollectionTreeRow.prototype.isUnfiled = function () {
|
||||||
return this.type == 'unfiled';
|
return this.type == 'unfiled';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Zotero.CollectionTreeRow.prototype.isRetracted = function () {
|
||||||
|
return this.type == 'retracted';
|
||||||
|
}
|
||||||
|
|
||||||
Zotero.CollectionTreeRow.prototype.isTrash = function()
|
Zotero.CollectionTreeRow.prototype.isTrash = function()
|
||||||
{
|
{
|
||||||
return this.type == 'trash';
|
return this.type == 'trash';
|
||||||
|
@ -162,7 +169,7 @@ Zotero.CollectionTreeRow.prototype.__defineGetter__('editable', function () {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
var libraryID = this.ref.libraryID;
|
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;
|
var type = Zotero.Libraries.get(libraryID).libraryType;
|
||||||
if (type == 'group') {
|
if (type == 'group') {
|
||||||
var groupID = Zotero.Groups.getGroupIDFromLibraryID(libraryID);
|
var groupID = Zotero.Groups.getGroupIDFromLibraryID(libraryID);
|
||||||
|
@ -185,7 +192,7 @@ Zotero.CollectionTreeRow.prototype.__defineGetter__('filesEditable', function ()
|
||||||
if (this.isGroup()) {
|
if (this.isGroup()) {
|
||||||
return this.ref.filesEditable;
|
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;
|
var type = Zotero.Libraries.get(libraryID).libraryType;
|
||||||
if (type == 'group') {
|
if (type == 'group') {
|
||||||
var groupID = Zotero.Groups.getGroupIDFromLibraryID(libraryID);
|
var groupID = Zotero.Groups.getGroupIDFromLibraryID(libraryID);
|
||||||
|
|
|
@ -183,6 +183,8 @@ Zotero.CollectionTreeView.prototype.refresh = Zotero.Promise.coroutine(function*
|
||||||
}
|
}
|
||||||
this._virtualCollectionLibraries.unfiled =
|
this._virtualCollectionLibraries.unfiled =
|
||||||
Zotero.Utilities.Internal.getVirtualCollectionState('unfiled')
|
Zotero.Utilities.Internal.getVirtualCollectionState('unfiled')
|
||||||
|
this._virtualCollectionLibraries.retracted =
|
||||||
|
Zotero.Utilities.Internal.getVirtualCollectionState('retracted');
|
||||||
|
|
||||||
var oldCount = this.rowCount || 0;
|
var oldCount = this.rowCount || 0;
|
||||||
var newRows = [];
|
var newRows = [];
|
||||||
|
@ -790,6 +792,9 @@ Zotero.CollectionTreeView.prototype.getImageSrc = function(row, col)
|
||||||
|
|
||||||
case 'publications':
|
case 'publications':
|
||||||
return "chrome://zotero/skin/treeitem-journalArticle" + suffix + ".png";
|
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";
|
return "chrome://zotero/skin/treesource-" + collectionType + suffix + ".png";
|
||||||
|
@ -823,6 +828,8 @@ Zotero.CollectionTreeView.prototype.isContainerEmpty = function(row)
|
||||||
|| this._virtualCollectionLibraries.duplicates[libraryID] === false)
|
|| this._virtualCollectionLibraries.duplicates[libraryID] === false)
|
||||||
// Unfiled Items not shown
|
// Unfiled Items not shown
|
||||||
&& this._virtualCollectionLibraries.unfiled[libraryID] === false
|
&& this._virtualCollectionLibraries.unfiled[libraryID] === false
|
||||||
|
// Retracted Items not shown
|
||||||
|
&& this._virtualCollectionLibraries.retracted[libraryID] === false
|
||||||
&& this.hideSources.indexOf('trash') != -1;
|
&& this.hideSources.indexOf('trash') != -1;
|
||||||
}
|
}
|
||||||
if (treeRow.isCollection()) {
|
if (treeRow.isCollection()) {
|
||||||
|
@ -1071,6 +1078,7 @@ Zotero.CollectionTreeView.prototype.selectByID = Zotero.Promise.coroutine(functi
|
||||||
|
|
||||||
case 'D':
|
case 'D':
|
||||||
case 'U':
|
case 'U':
|
||||||
|
case 'R':
|
||||||
yield this.expandLibrary(id);
|
yield this.expandLibrary(id);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
@ -1326,6 +1334,8 @@ Zotero.CollectionTreeView.prototype._expandRow = Zotero.Promise.coroutine(functi
|
||||||
var showDuplicates = this.hideSources.indexOf('duplicates') == -1
|
var showDuplicates = this.hideSources.indexOf('duplicates') == -1
|
||||||
&& this._virtualCollectionLibraries.duplicates[libraryID] !== false;
|
&& this._virtualCollectionLibraries.duplicates[libraryID] !== false;
|
||||||
var showUnfiled = this._virtualCollectionLibraries.unfiled[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 showPublications = libraryID == Zotero.Libraries.userLibraryID;
|
||||||
var showTrash = this.hideSources.indexOf('trash') == -1;
|
var showTrash = this.hideSources.indexOf('trash') == -1;
|
||||||
}
|
}
|
||||||
|
@ -1333,6 +1343,7 @@ Zotero.CollectionTreeView.prototype._expandRow = Zotero.Promise.coroutine(functi
|
||||||
var savedSearches = [];
|
var savedSearches = [];
|
||||||
var showDuplicates = false;
|
var showDuplicates = false;
|
||||||
var showUnfiled = false;
|
var showUnfiled = false;
|
||||||
|
var showRetracted = false;
|
||||||
var showPublications = false;
|
var showPublications = false;
|
||||||
var showTrash = false;
|
var showTrash = false;
|
||||||
}
|
}
|
||||||
|
@ -1346,7 +1357,7 @@ Zotero.CollectionTreeView.prototype._expandRow = Zotero.Promise.coroutine(functi
|
||||||
return 0;
|
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
|
// If this isn't a manual open, set the initial state depending on whether
|
||||||
// there are child nodes
|
// there are child nodes
|
||||||
|
@ -1430,6 +1441,21 @@ Zotero.CollectionTreeView.prototype._expandRow = Zotero.Promise.coroutine(functi
|
||||||
newRows++;
|
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) {
|
if (showTrash) {
|
||||||
let deletedItems = yield Zotero.Items.getDeleted(libraryID);
|
let deletedItems = yield Zotero.Items.getDeleted(libraryID);
|
||||||
if (deletedItems.length || Zotero.Prefs.get("showTrashWhenEmpty")) {
|
if (deletedItems.length || Zotero.Prefs.get("showTrashWhenEmpty")) {
|
||||||
|
|
|
@ -972,6 +972,10 @@ Zotero.Search.prototype._buildQuery = Zotero.Promise.coroutine(function* () {
|
||||||
var unfiled = condition.operator == 'true';
|
var unfiled = condition.operator == 'true';
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
case 'retracted':
|
||||||
|
var retracted = condition.operator == 'true';
|
||||||
|
continue;
|
||||||
|
|
||||||
case 'publications':
|
case 'publications':
|
||||||
var publications = condition.operator == 'true';
|
var publications = condition.operator == 'true';
|
||||||
continue;
|
continue;
|
||||||
|
@ -1034,6 +1038,10 @@ Zotero.Search.prototype._buildQuery = Zotero.Promise.coroutine(function* () {
|
||||||
+ "AND itemID NOT IN (SELECT itemID FROM publicationsItems)";
|
+ "AND itemID NOT IN (SELECT itemID FROM publicationsItems)";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (retracted) {
|
||||||
|
sql += " AND (itemID IN (SELECT itemID FROM retractedItems))";
|
||||||
|
}
|
||||||
|
|
||||||
if (publications) {
|
if (publications) {
|
||||||
sql += " AND (itemID IN (SELECT itemID FROM publicationsItems))";
|
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',
|
name: 'publications',
|
||||||
operators: {
|
operators: {
|
||||||
|
|
|
@ -44,13 +44,17 @@ Zotero.Retractions = {
|
||||||
_keyItems: {},
|
_keyItems: {},
|
||||||
_itemKeys: {},
|
_itemKeys: {},
|
||||||
|
|
||||||
|
_retractedItems: new Set(),
|
||||||
|
_retractedItemsByLibrary: {},
|
||||||
|
_librariesWithRetractions: new Set(),
|
||||||
|
|
||||||
init: async function () {
|
init: async function () {
|
||||||
this._cacheFile = OS.Path.join(Zotero.Profile.dir, 'retractions.json');
|
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
|
// 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().
|
// item changes so they can be kept up to date in notify().
|
||||||
await this._cacheKeyMappings();
|
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
|
// Load in the cached prefix list that we check new items against
|
||||||
try {
|
try {
|
||||||
|
@ -62,8 +66,20 @@ Zotero.Retractions = {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load existing retracted items
|
// Load existing retracted items
|
||||||
var itemIDs = await Zotero.DB.columnQueryAsync("SELECT itemID FROM retractedItems");
|
var rows = await Zotero.DB.queryAsync(
|
||||||
this._retractedItems = new Set(itemIDs);
|
"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;
|
this._initialized = true;
|
||||||
|
|
||||||
|
@ -81,6 +97,55 @@ Zotero.Retractions = {
|
||||||
return this._retractedItems.has(item.id);
|
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
|
* Return retraction data for an item
|
||||||
*
|
*
|
||||||
|
@ -127,7 +192,16 @@ Zotero.Retractions = {
|
||||||
return description;
|
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') {
|
if (action == 'add') {
|
||||||
for (let id of ids) {
|
for (let id of ids) {
|
||||||
this._updateItem(Zotero.Items.get(id));
|
this._updateItem(Zotero.Items.get(id));
|
||||||
|
@ -140,7 +214,7 @@ Zotero.Retractions = {
|
||||||
let typeID = this['TYPE_' + type];
|
let typeID = this['TYPE_' + type];
|
||||||
let fieldVal = this['_getItem' + type](item);
|
let fieldVal = this['_getItem' + type](item);
|
||||||
if (fieldVal) {
|
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 key = this._itemKeys[typeID].get(item.id);
|
||||||
let newKey = this._valueToKey(typeID, fieldVal);
|
let newKey = this._valueToKey(typeID, fieldVal);
|
||||||
if (key != newKey) {
|
if (key != newKey) {
|
||||||
|
@ -149,18 +223,31 @@ Zotero.Retractions = {
|
||||||
continue;
|
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)) {
|
else if (this._itemKeys[typeID].get(item.id)) {
|
||||||
this._deleteItemKeyMappings(id);
|
this._deleteItemKeyMappings(id);
|
||||||
this._updateItem(item);
|
this._updateItem(item);
|
||||||
continue;
|
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') {
|
else if (action == 'delete') {
|
||||||
for (let id of ids) {
|
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)
|
* Check for possible matches for items in the queue (debounced)
|
||||||
*/
|
*/
|
||||||
checkQueuedItems: Zotero.Utilities.debounce(async function () {
|
checkQueuedItems: Zotero.Utilities.debounce(async function () {
|
||||||
|
return this._checkQueuedItemsInternal();
|
||||||
|
}, 1000),
|
||||||
|
|
||||||
|
_checkQueuedItemsInternal: async function () {
|
||||||
Zotero.debug("Checking updated items for retractions");
|
Zotero.debug("Checking updated items for retractions");
|
||||||
|
|
||||||
// If no possible matches, clear retraction flag on any items that changed
|
// If no possible matches, clear retraction flag on any items that changed
|
||||||
if (!this._queuedPrefixStrings.size) {
|
if (!this._queuedPrefixStrings.size) {
|
||||||
for (let item of this._queuedItems) {
|
for (let item of this._queuedItems) {
|
||||||
await this._removeEntry(item.id);
|
await this._removeEntry(item.id, item.libraryID);
|
||||||
}
|
}
|
||||||
this._queuedItems.clear();
|
this._queuedItems.clear();
|
||||||
return;
|
return;
|
||||||
|
@ -202,10 +293,10 @@ Zotero.Retractions = {
|
||||||
// Remove retraction status for items that were checked but didn't match
|
// Remove retraction status for items that were checked but didn't match
|
||||||
for (let item of items) {
|
for (let item of items) {
|
||||||
if (!addedItems.includes(item.id)) {
|
if (!addedItems.includes(item.id)) {
|
||||||
await this._removeEntry(item.id);
|
await this._removeEntry(item.id, item.libraryID);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, 1000),
|
},
|
||||||
|
|
||||||
updateFromServer: Zotero.serial(async function () {
|
updateFromServer: Zotero.serial(async function () {
|
||||||
if (!this._initialized) {
|
if (!this._initialized) {
|
||||||
|
@ -231,7 +322,12 @@ Zotero.Retractions = {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var etag = req.getResponseHeader('ETag');
|
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
|
// Calculate prefix length automatically
|
||||||
var doiPrefixLength;
|
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");
|
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);
|
await this._saveCacheFile(list, etag, doiPrefixLength, pmidPrefixLength);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
@ -492,7 +586,7 @@ Zotero.Retractions = {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._queuedItems.add(item);
|
this._queuedItems.add(item);
|
||||||
var doi = this._getItemDOI(item);
|
let doi = this._getItemDOI(item);
|
||||||
if (doi) {
|
if (doi) {
|
||||||
this._addItemKeyMapping(this.TYPE_DOI, doi, item.id);
|
this._addItemKeyMapping(this.TYPE_DOI, doi, item.id);
|
||||||
let prefixStr = this.TYPE_DOI + this._getDOIPrefix(doi, this._cacheDOIPrefixLength);
|
let prefixStr = this.TYPE_DOI + this._getDOIPrefix(doi, this._cacheDOIPrefixLength);
|
||||||
|
@ -500,7 +594,7 @@ Zotero.Retractions = {
|
||||||
this._queuedPrefixStrings.add(prefixStr);
|
this._queuedPrefixStrings.add(prefixStr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var pmid = this._getItemPMID(item);
|
let pmid = this._getItemPMID(item);
|
||||||
if (pmid) {
|
if (pmid) {
|
||||||
this._addItemKeyMapping(this.TYPE_PMID, pmid, item.id);
|
this._addItemKeyMapping(this.TYPE_PMID, pmid, item.id);
|
||||||
let prefixStr = this.TYPE_PMID + this._getPMIDPrefix(pmid, this._cachePMIDPrefixLength);
|
let prefixStr = this.TYPE_PMID + this._getPMIDPrefix(pmid, this._cachePMIDPrefixLength);
|
||||||
|
@ -523,19 +617,31 @@ Zotero.Retractions = {
|
||||||
var sql = "REPLACE INTO retractedItems VALUES (?, ?)";
|
var sql = "REPLACE INTO retractedItems VALUES (?, ?)";
|
||||||
await Zotero.DB.queryAsync(sql, [itemID, JSON.stringify(o)]);
|
await Zotero.DB.queryAsync(sql, [itemID, JSON.stringify(o)]);
|
||||||
|
|
||||||
|
var item = Zotero.Items.get(itemID);
|
||||||
|
var libraryID = item.libraryID;
|
||||||
this._retractedItems.add(itemID);
|
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]);
|
await Zotero.Notifier.trigger('refresh', 'item', [itemID]);
|
||||||
},
|
},
|
||||||
|
|
||||||
_removeEntry: async function (itemID) {
|
_removeEntry: async function (itemID, libraryID) {
|
||||||
this._deleteItemKeyMappings(itemID);
|
this._deleteItemKeyMappings(itemID);
|
||||||
|
|
||||||
if (this._retractedItems.has(itemID)) {
|
if (!this._retractedItems.has(itemID)) {
|
||||||
await Zotero.DB.queryAsync("DELETE FROM retractedItems WHERE itemID=?", itemID);
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await Zotero.DB.queryAsync("DELETE FROM retractedItems WHERE itemID=?", itemID);
|
||||||
this._retractedItems.delete(itemID);
|
this._retractedItems.delete(itemID);
|
||||||
|
this._retractedItemsByLibrary[libraryID].delete(itemID);
|
||||||
|
await this._updateLibraryRetractions(libraryID);
|
||||||
|
|
||||||
await Zotero.Notifier.trigger('refresh', 'item', [itemID]);
|
await Zotero.Notifier.trigger('refresh', 'item', [itemID]);
|
||||||
},
|
},
|
||||||
|
|
|
@ -1410,6 +1410,10 @@ Zotero.Utilities.Internal = {
|
||||||
var prefKey = 'unfiledLibraries';
|
var prefKey = 'unfiledLibraries';
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'retracted':
|
||||||
|
var prefKey = 'retractedLibraries';
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error("Invalid virtual collection type '" + type + "'");
|
throw new Error("Invalid virtual collection type '" + type + "'");
|
||||||
}
|
}
|
||||||
|
@ -1445,6 +1449,10 @@ Zotero.Utilities.Internal = {
|
||||||
var prefKey = 'unfiledLibraries';
|
var prefKey = 'unfiledLibraries';
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'retracted':
|
||||||
|
var prefKey = 'retractedLibraries';
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error("Invalid virtual collection type '" + type + "'");
|
throw new Error("Invalid virtual collection type '" + type + "'");
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,6 +71,17 @@ Services.scriptloader.loadSubScript("resource://zotero/polyfill.js");
|
||||||
return win ? win.ZoteroPane : null;
|
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
|
* @property {Boolean} locked Whether all Zotero panes are locked
|
||||||
* with an overlay
|
* 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) {
|
switch (type) {
|
||||||
case 'duplicates':
|
case 'duplicates':
|
||||||
var treeViewID = 'D' + libraryID;
|
var treeViewID = 'D' + libraryID;
|
||||||
|
@ -1037,6 +1037,10 @@ var ZoteroPane = new function()
|
||||||
var treeViewID = 'U' + libraryID;
|
var treeViewID = 'U' + libraryID;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'retracted':
|
||||||
|
var treeViewID = 'R' + libraryID;
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error("Invalid virtual collection type '" + type + "'");
|
throw new Error("Invalid virtual collection type '" + type + "'");
|
||||||
}
|
}
|
||||||
|
@ -1046,13 +1050,17 @@ var ZoteroPane = new function()
|
||||||
var cv = this.collectionsView;
|
var cv = this.collectionsView;
|
||||||
|
|
||||||
var promise = cv.waitForSelect();
|
var promise = cv.waitForSelect();
|
||||||
|
var selectedRowID = cv.selectedTreeRow.id;
|
||||||
var selectedRow = cv.selection.currentIndex;
|
var selectedRow = cv.selection.currentIndex;
|
||||||
|
|
||||||
yield cv.refresh();
|
yield cv.refresh();
|
||||||
|
|
||||||
// Select new row
|
// Select new or original row
|
||||||
if (show) {
|
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
|
// Select next appropriate row after removal
|
||||||
else {
|
else {
|
||||||
|
@ -1791,7 +1799,10 @@ var ZoteroPane = new function()
|
||||||
|
|
||||||
var prompt = force ? toTrash : toRemove;
|
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) {
|
if (!force) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -1865,6 +1876,11 @@ var ZoteroPane = new function()
|
||||||
this.setVirtual(collectionTreeRow.ref.libraryID, 'unfiled', false);
|
this.setVirtual(collectionTreeRow.ref.libraryID, 'unfiled', false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Remove virtual retracted collection
|
||||||
|
else if (collectionTreeRow.isRetracted()) {
|
||||||
|
this.setVirtual(collectionTreeRow.ref.libraryID, 'retracted', false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.canEdit() && !collectionTreeRow.isFeed()) {
|
if (!this.canEdit() && !collectionTreeRow.isFeed()) {
|
||||||
this.displayCannotEditLibraryMessage();
|
this.displayCannotEditLibraryMessage();
|
||||||
|
@ -2401,13 +2417,19 @@ var ZoteroPane = new function()
|
||||||
{
|
{
|
||||||
id: "showDuplicates",
|
id: "showDuplicates",
|
||||||
oncommand: () => {
|
oncommand: () => {
|
||||||
this.setVirtual(this.getSelectedLibraryID(), 'duplicates', true);
|
this.setVirtual(this.getSelectedLibraryID(), 'duplicates', true, true);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "showUnfiled",
|
id: "showUnfiled",
|
||||||
oncommand: () => {
|
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()) {
|
else if (collectionTreeRow.isTrash()) {
|
||||||
show = ['emptyTrash'];
|
show = ['emptyTrash'];
|
||||||
}
|
}
|
||||||
else if (collectionTreeRow.isDuplicates() || collectionTreeRow.isUnfiled()) {
|
else if (collectionTreeRow.isDuplicates() || collectionTreeRow.isUnfiled() || collectionTreeRow.isRetracted()) {
|
||||||
show = ['deleteCollection'];
|
show = ['deleteCollection'];
|
||||||
|
|
||||||
m.deleteCollection.setAttribute('label', Zotero.getString('general.hide'));
|
m.deleteCollection.setAttribute('label', Zotero.getString('general.hide'));
|
||||||
|
@ -2624,14 +2646,17 @@ var ZoteroPane = new function()
|
||||||
'newSavedSearch'
|
'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(
|
let duplicates = Zotero.Utilities.Internal.getVirtualCollectionStateForLibrary(
|
||||||
libraryID, 'duplicates'
|
libraryID, 'duplicates'
|
||||||
);
|
);
|
||||||
let unfiled = Zotero.Utilities.Internal.getVirtualCollectionStateForLibrary(
|
let unfiled = Zotero.Utilities.Internal.getVirtualCollectionStateForLibrary(
|
||||||
libraryID, 'unfiled'
|
libraryID, 'unfiled'
|
||||||
);
|
);
|
||||||
if (!duplicates || !unfiled) {
|
let retracted = Zotero.Utilities.Internal.getVirtualCollectionStateForLibrary(
|
||||||
|
libraryID, 'retracted'
|
||||||
|
);
|
||||||
|
if (!duplicates || !unfiled || !retracted) {
|
||||||
if (!library.archived) {
|
if (!library.archived) {
|
||||||
show.push('sep2');
|
show.push('sep2');
|
||||||
}
|
}
|
||||||
|
@ -2641,6 +2666,9 @@ var ZoteroPane = new function()
|
||||||
if (!unfiled) {
|
if (!unfiled) {
|
||||||
show.push('showUnfiled');
|
show.push('showUnfiled');
|
||||||
}
|
}
|
||||||
|
if (!retracted) {
|
||||||
|
show.push('showRetracted');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!library.archived) {
|
if (!library.archived) {
|
||||||
show.push('sep3');
|
show.push('sep3');
|
||||||
|
@ -2656,7 +2684,11 @@ var ZoteroPane = new function()
|
||||||
// Disable some actions if user doesn't have write access
|
// Disable some actions if user doesn't have write access
|
||||||
//
|
//
|
||||||
// Some actions are disabled via their commands in onCollectionSelected()
|
// 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(
|
disable.push(
|
||||||
'newSubcollection',
|
'newSubcollection',
|
||||||
'editSelectedCollection',
|
'editSelectedCollection',
|
||||||
|
@ -3219,8 +3251,8 @@ var ZoteroPane = new function()
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ignore double-clicks on Unfiled Items source row
|
// Ignore double-clicks on Unfiled/Retracted Items source rows
|
||||||
if (collectionTreeRow.isUnfiled()) {
|
if (collectionTreeRow.isUnfiled() || collectionTreeRow.isRetracted()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4762,12 +4794,15 @@ var ZoteroPane = new function()
|
||||||
var suffix = items.length > 1 ? 'multiple' : 'single';
|
var suffix = items.length > 1 ? 'multiple' : 'single';
|
||||||
message.textContent = Zotero.getString('retraction.alert.' + suffix);
|
message.textContent = Zotero.getString('retraction.alert.' + suffix);
|
||||||
link.textContent = Zotero.getString('retraction.alert.view.' + suffix);
|
link.textContent = Zotero.getString('retraction.alert.view.' + suffix);
|
||||||
link.onclick = function () {
|
link.onclick = async 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);
|
|
||||||
this.hideRetractionBanner();
|
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);
|
}.bind(this);
|
||||||
|
|
||||||
close.onclick = function () {
|
close.onclick = function () {
|
||||||
|
|
|
@ -216,7 +216,7 @@
|
||||||
</toolbar>
|
</toolbar>
|
||||||
|
|
||||||
<vbox id="retracted-items-container" collapsed="true">
|
<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-message"/>
|
||||||
<div id="retracted-items-link"/>
|
<div id="retracted-items-link"/>
|
||||||
<div id="retracted-items-close">×</div>
|
<div id="retracted-items-close">×</div>
|
||||||
|
@ -236,6 +236,7 @@
|
||||||
<menuseparator/>
|
<menuseparator/>
|
||||||
<menuitem class="zotero-menuitem-show-duplicates" label="&zotero.toolbar.duplicate.label;"/>
|
<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-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-edit-collection"/>
|
||||||
<menuitem class="zotero-menuitem-duplicate-collection"/>
|
<menuitem class="zotero-menuitem-duplicate-collection"/>
|
||||||
<menuitem class="zotero-menuitem-mark-read-feed" label="&zotero.toolbar.markFeedRead.label;"/>
|
<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.toolbar.duplicate.label "Show Duplicates">
|
||||||
<!ENTITY zotero.collections.showUnfiledItems "Show Unfiled Items">
|
<!ENTITY zotero.collections.showUnfiledItems "Show Unfiled Items">
|
||||||
|
<!ENTITY zotero.collections.showRetractedItems "Show Retracted Items">
|
||||||
|
|
||||||
<!ENTITY zotero.items.itemType "Item Type">
|
<!ENTITY zotero.items.itemType "Item Type">
|
||||||
<!ENTITY zotero.items.type_column "Item Type">
|
<!ENTITY zotero.items.type_column "Item Type">
|
||||||
|
|
|
@ -234,6 +234,7 @@ pane.collections.feedLibraries = Feeds
|
||||||
pane.collections.trash = Trash
|
pane.collections.trash = Trash
|
||||||
pane.collections.untitled = Untitled
|
pane.collections.untitled = Untitled
|
||||||
pane.collections.unfiled = Unfiled Items
|
pane.collections.unfiled = Unfiled Items
|
||||||
|
pane.collections.retracted = Retracted Items
|
||||||
pane.collections.duplicate = Duplicate Items
|
pane.collections.duplicate = Duplicate Items
|
||||||
pane.collections.removeLibrary = Remove Library
|
pane.collections.removeLibrary = Remove Library
|
||||||
pane.collections.removeLibrary.text = Are you sure you want to permanently remove “%S” from this computer?
|
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');
|
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 {
|
.zotero-menuitem-sync {
|
||||||
list-style-image: url(chrome://zotero/skin/arrow_rotate_static.png);
|
list-style-image: url(chrome://zotero/skin/arrow_rotate_static.png);
|
||||||
}
|
}
|
||||||
|
@ -764,6 +768,7 @@
|
||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* BEGIN 2X BLOCK -- DO NOT EDIT MANUALLY -- USE 2XIZE */
|
/* BEGIN 2X BLOCK -- DO NOT EDIT MANUALLY -- USE 2XIZE */
|
||||||
@media (min-resolution: 1.25dppx) {
|
@media (min-resolution: 1.25dppx) {
|
||||||
#zotero-items-column-hasAttachment { list-style-image: url(chrome://zotero/skin/attach-small@2x.png); }
|
#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-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-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-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-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-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'); }
|
.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 () {
|
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('duplicateLibraries');
|
||||||
Zotero.Prefs.clear('unfiledLibraries');
|
Zotero.Prefs.clear('unfiledLibraries');
|
||||||
|
Zotero.Prefs.clear('retractedLibraries');
|
||||||
yield cv.refresh();
|
yield cv.refresh();
|
||||||
assert.ok(cv.getRowIndexByID("D" + userLibraryID));
|
assert.ok(cv.getRowIndexByID("D" + userLibraryID));
|
||||||
assert.ok(cv.getRowIndexByID("U" + 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('duplicateLibraries', `{"${userLibraryID}": false}`);
|
||||||
Zotero.Prefs.set('unfiledLibraries', `{"${userLibraryID}": false}`);
|
Zotero.Prefs.set('unfiledLibraries', `{"${userLibraryID}": false}`);
|
||||||
|
Zotero.Prefs.set('retractedLibraries', `{"${userLibraryID}": false}`);
|
||||||
yield cv.refresh();
|
yield cv.refresh();
|
||||||
assert.isFalse(cv.getRowIndexByID("D" + userLibraryID));
|
assert.isFalse(cv.getRowIndexByID("D" + userLibraryID));
|
||||||
assert.isFalse(cv.getRowIndexByID("U" + userLibraryID));
|
assert.isFalse(cv.getRowIndexByID("U" + userLibraryID));
|
||||||
|
assert.isFalse(cv.getRowIndexByID("R" + userLibraryID));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should maintain open state of group", function* () {
|
it("should maintain open state of group", function* () {
|
||||||
|
|
|
@ -595,9 +595,8 @@ describe("Zotero.ItemTreeView", function() {
|
||||||
var userLibraryID = Zotero.Libraries.userLibraryID;
|
var userLibraryID = Zotero.Libraries.userLibraryID;
|
||||||
var collection = yield createDataObject('collection');
|
var collection = yield createDataObject('collection');
|
||||||
var item = yield createDataObject('item', { title: "Unfiled Item" });
|
var item = yield createDataObject('item', { title: "Unfiled Item" });
|
||||||
yield zp.setVirtual(userLibraryID, 'unfiled', true);
|
yield zp.setVirtual(userLibraryID, 'unfiled', true, true);
|
||||||
var selected = yield cv.selectByID("U" + userLibraryID);
|
assert.equal(cv.selectedTreeRow.id, 'U' + userLibraryID);
|
||||||
assert.ok(selected);
|
|
||||||
yield waitForItemsLoad(win);
|
yield waitForItemsLoad(win);
|
||||||
assert.isNumber(zp.itemsView.getRowIndexByID(item.id));
|
assert.isNumber(zp.itemsView.getRowIndexByID(item.id));
|
||||||
yield Zotero.DB.executeTransaction(function* () {
|
yield Zotero.DB.executeTransaction(function* () {
|
||||||
|
|
|
@ -1,34 +1,93 @@
|
||||||
describe("Retractions", function() {
|
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 () {
|
describe("Notification Banner", function () {
|
||||||
var win;
|
function bannerShown() {
|
||||||
var zp;
|
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 () {
|
it("should show banner when retracted item is added", async function () {
|
||||||
win = await loadZoteroPane();
|
var banner = win.document.getElementById('retracted-items-container');
|
||||||
zp = win.ZoteroPane;
|
assert.isFalse(bannerShown());
|
||||||
|
|
||||||
|
await createRetractedItem();
|
||||||
|
|
||||||
|
assert.isTrue(bannerShown());
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(function () {
|
it("shouldn't show banner when item in trash is added", async function () {
|
||||||
win.document.getElementById('retracted-items-close').click();
|
var item = await createRetractedItem({ deleted: true });
|
||||||
});
|
|
||||||
|
|
||||||
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 });
|
|
||||||
|
|
||||||
await Zotero.Retractions._addEntry(item1.id, {});
|
assert.isFalse(bannerShown());
|
||||||
await Zotero.Retractions._addEntry(item2.id, {});
|
|
||||||
await Zotero.Retractions._addEntry(item3.id, {});
|
|
||||||
|
|
||||||
await createDataObject('collection');
|
|
||||||
await waitForItemsLoad(win);
|
|
||||||
|
|
||||||
await Zotero.Retractions._showAlert([item1.id, item2.id, item3.id]);
|
|
||||||
win.document.getElementById('retracted-items-link').click();
|
win.document.getElementById('retracted-items-link').click();
|
||||||
|
|
||||||
while (zp.collectionsView.selectedTreeRow.id != 'L1') {
|
while (zp.collectionsView.selectedTreeRow.id != 'L1') {
|
||||||
|
@ -37,7 +96,66 @@ describe("Retractions", function() {
|
||||||
await waitForItemsLoad(win);
|
await waitForItemsLoad(win);
|
||||||
|
|
||||||
var item = await zp.getSelectedItems()[0];
|
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
|
// Show Duplicate Items
|
||||||
var id = "D" + userLibraryID;
|
var id = "D" + userLibraryID;
|
||||||
assert.isFalse(cv.getRowIndexByID(id));
|
assert.isFalse(cv.getRowIndexByID(id));
|
||||||
yield zp.setVirtual(userLibraryID, 'duplicates', true);
|
yield zp.setVirtual(userLibraryID, 'duplicates', true, true);
|
||||||
// Duplicate Items should be selected
|
// Duplicate Items should be selected
|
||||||
assert.equal(cv.selectedTreeRow.id, id);
|
assert.equal(cv.selectedTreeRow.id, id);
|
||||||
// Should be missing from pref
|
// Should be missing from pref
|
||||||
|
@ -490,7 +490,7 @@ describe("ZoteroPane", function() {
|
||||||
// Show Unfiled Items
|
// Show Unfiled Items
|
||||||
id = "U" + userLibraryID;
|
id = "U" + userLibraryID;
|
||||||
assert.isFalse(cv.getRowIndexByID(id));
|
assert.isFalse(cv.getRowIndexByID(id));
|
||||||
yield zp.setVirtual(userLibraryID, 'unfiled', true);
|
yield zp.setVirtual(userLibraryID, 'unfiled', true, true);
|
||||||
// Unfiled Items should be selected
|
// Unfiled Items should be selected
|
||||||
assert.equal(cv.selectedTreeRow.id, id);
|
assert.equal(cv.selectedTreeRow.id, id);
|
||||||
// Should be missing from pref
|
// Should be missing from pref
|
||||||
|
@ -510,7 +510,7 @@ describe("ZoteroPane", function() {
|
||||||
|
|
||||||
// Show Duplicate Items
|
// Show Duplicate Items
|
||||||
var id = "D" + userLibraryID;
|
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
|
// Library should have been expanded and Duplicate Items selected
|
||||||
assert.ok(cv.getRowIndexByID(id));
|
assert.ok(cv.getRowIndexByID(id));
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue