Add Retracted Items virtual collection

Shown automatically when retracted items are detected
This commit is contained in:
Dan Stillman 2019-06-10 02:37:54 -04:00
parent 502f5fe491
commit 5c03813d81
16 changed files with 417 additions and 78 deletions

View file

@ -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);

View file

@ -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")) {

View file

@ -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))";
}

View file

@ -99,6 +99,14 @@ Zotero.SearchConditions = new function(){
}
},
{
name: 'retracted',
operators: {
true: true,
false: true
}
},
{
name: 'publications',
operators: {

View file

@ -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]);
},

View file

@ -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 + "'");
}

View file

@ -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

View file

@ -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 () {

View file

@ -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;"/>

View file

@ -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">

View file

@ -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?

View file

@ -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'); }

View file

@ -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* () {

View file

@ -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* () {

View file

@ -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));
});
});
});

View file

@ -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));