Add notification banner when retracted items are found
And other retraction tweaks
This commit is contained in:
parent
de0a65af7b
commit
7f4f2770ba
8 changed files with 336 additions and 169 deletions
|
@ -4,6 +4,7 @@ Zotero.Retractions = {
|
|||
TYPE_NAMES: ['DOI', 'PMID'],
|
||||
|
||||
_version: 1,
|
||||
|
||||
_cacheFile: null,
|
||||
_cacheETag: null,
|
||||
_cacheDOIPrefixLength: null,
|
||||
|
@ -19,9 +20,12 @@ Zotero.Retractions = {
|
|||
init: async function () {
|
||||
this._cacheFile = OS.Path.join(Zotero.Profile.dir, 'retractions.json');
|
||||
|
||||
await this._cacheItems();
|
||||
// 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');
|
||||
|
||||
// Load in the cached prefix list that we check new items against
|
||||
try {
|
||||
await this._loadCacheFile();
|
||||
}
|
||||
|
@ -30,8 +34,9 @@ Zotero.Retractions = {
|
|||
Zotero.logError(e);
|
||||
}
|
||||
|
||||
// Load existing retracted items
|
||||
var itemIDs = await Zotero.DB.columnQueryAsync("SELECT itemID FROM retractedItems");
|
||||
this.retractedItems = new Set(itemIDs);
|
||||
this._retractedItems = new Set(itemIDs);
|
||||
|
||||
// TEMP
|
||||
Zotero.Schema.schemaUpdatePromise.then(() => {
|
||||
|
@ -39,144 +44,20 @@ Zotero.Retractions = {
|
|||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* @param {Zotero.Item}
|
||||
* @return {Boolean}
|
||||
*/
|
||||
isRetracted: function (item) {
|
||||
return this.retractedItems.has(item.id);
|
||||
return this._retractedItems.has(item.id);
|
||||
},
|
||||
|
||||
|
||||
notify: async function (action, type, ids, extraData) {
|
||||
if (action == 'add') {
|
||||
for (let id of ids) {
|
||||
this._updateItem(Zotero.Items.get(id));
|
||||
}
|
||||
}
|
||||
else if (action == 'modify') {
|
||||
for (let id of ids) {
|
||||
let item = Zotero.Items.get(id);
|
||||
for (let type of this.TYPE_NAMES) {
|
||||
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
|
||||
let key = this._itemKeys[typeID].get(item.id);
|
||||
let newKey = this._valueToKey(typeID, fieldVal);
|
||||
if (key != newKey) {
|
||||
this._deleteItemKeyMappings(id);
|
||||
this._updateItem(item);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// If a previous key value was cleared, re-map
|
||||
else if (this._itemKeys[typeID].get(item.id)) {
|
||||
this._deleteItemKeyMappings(id);
|
||||
this._updateItem(item);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (action == 'delete') {
|
||||
for (let id of ids) {
|
||||
await this.removeEntry(id);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Check for possible matches for items in the queue (debounced)
|
||||
* Return retraction data for an item
|
||||
*
|
||||
* @param {Zotero.Item} item
|
||||
* @return {Object|false}
|
||||
*/
|
||||
checkQueuedItems: Zotero.Utilities.debounce(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);
|
||||
}
|
||||
this._queuedItems.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
var items = [...this._queuedItems];
|
||||
var prefixStrings = [...this._queuedPrefixStrings];
|
||||
this._queuedItems.clear();
|
||||
this._queuedPrefixStrings.clear();
|
||||
var addedItems = [];
|
||||
try {
|
||||
addedItems = await this._downloadPossibleMatches(prefixStrings);
|
||||
}
|
||||
catch (e) {
|
||||
// Add back to queue on failure
|
||||
for (let item of items) {
|
||||
this._queuedItems.add(item);
|
||||
}
|
||||
for (let prefixStr of prefixStrings) {
|
||||
this._queuedPrefixStrings.add(prefixStr);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}, 5000),
|
||||
|
||||
|
||||
/**
|
||||
* @return {Number[]} - Array of added item ids
|
||||
*/
|
||||
_downloadPossibleMatches: async function (prefixStrings) {
|
||||
var urlPrefix = (Zotero.Prefs.get("api.url") || ZOTERO_CONFIG.API_URL) + '/retractions/';
|
||||
var req = await Zotero.HTTP.request(
|
||||
"POST",
|
||||
urlPrefix + 'search',
|
||||
{
|
||||
body: JSON.stringify(prefixStrings),
|
||||
responseType: 'json'
|
||||
}
|
||||
);
|
||||
var results = req.response;
|
||||
Zotero.debug(`Retrieved ${results.length} possible `
|
||||
+ Zotero.Utilities.pluralize(results.length, ['match', 'matches']));
|
||||
|
||||
// Look for local items that match
|
||||
var addedItems = new Set();
|
||||
for (let row of results) {
|
||||
if (row.doi) {
|
||||
let ids = this._keyItems[this.TYPE_DOI].get(row.doi);
|
||||
if (ids) {
|
||||
for (let id of ids) {
|
||||
addedItems.add(id);
|
||||
await this.addEntry(id, row);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (row.pmid) {
|
||||
let ids = this._keyItems[this.TYPE_PMID].get(row.pmid.toString());
|
||||
if (ids) {
|
||||
for (let id of ids) {
|
||||
addedItems.add(id);
|
||||
await this.addEntry(id, row);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Zotero.debug(`Found ${addedItems.size} retracted `
|
||||
+ Zotero.Utilities.pluralize(addedItems.size, 'item'));
|
||||
|
||||
return [...addedItems];
|
||||
},
|
||||
|
||||
getData: async function (item) {
|
||||
var data = await Zotero.DB.valueQueryAsync(
|
||||
"SELECT data FROM retractedItems WHERE itemID=?", item.id
|
||||
|
@ -217,38 +98,87 @@ Zotero.Retractions = {
|
|||
return description;
|
||||
},
|
||||
|
||||
addEntry: async function (itemID, data) {
|
||||
var o = {};
|
||||
Object.assign(o, data);
|
||||
// Replace original ids with retraction ids
|
||||
if (data.retractionDOI) o.doi = data.retractionDOI;
|
||||
if (data.retractionPMID) o.pmid = data.retractionPMID;
|
||||
delete o.retractionDOI;
|
||||
delete o.retractionPMID;
|
||||
|
||||
var sql = "REPLACE INTO retractedItems VALUES (?, ?)";
|
||||
await Zotero.DB.queryAsync(sql, [itemID, JSON.stringify(o)]);
|
||||
|
||||
this.retractedItems.add(itemID);
|
||||
|
||||
await Zotero.Notifier.trigger('refresh', 'item', [itemID]);
|
||||
notify: async function (action, type, ids, _extraData) {
|
||||
if (action == 'add') {
|
||||
for (let id of ids) {
|
||||
this._updateItem(Zotero.Items.get(id));
|
||||
}
|
||||
}
|
||||
else if (action == 'modify') {
|
||||
for (let id of ids) {
|
||||
let item = Zotero.Items.get(id);
|
||||
for (let type of this.TYPE_NAMES) {
|
||||
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
|
||||
let key = this._itemKeys[typeID].get(item.id);
|
||||
let newKey = this._valueToKey(typeID, fieldVal);
|
||||
if (key != newKey) {
|
||||
this._deleteItemKeyMappings(id);
|
||||
this._updateItem(item);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// If a previous key value was cleared, re-map
|
||||
else if (this._itemKeys[typeID].get(item.id)) {
|
||||
this._deleteItemKeyMappings(id);
|
||||
this._updateItem(item);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (action == 'delete') {
|
||||
for (let id of ids) {
|
||||
await this._removeEntry(id);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
removeEntry: async function (itemID) {
|
||||
this._deleteItemKeyMappings(itemID);
|
||||
/**
|
||||
* Check for possible matches for items in the queue (debounced)
|
||||
*/
|
||||
checkQueuedItems: Zotero.Utilities.debounce(async function () {
|
||||
Zotero.debug("Checking updated items for retractions");
|
||||
|
||||
if (this.retractedItems.has(itemID)) {
|
||||
await Zotero.DB.queryAsync("DELETE FROM retractedItems WHERE itemID=?", itemID);
|
||||
// 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);
|
||||
}
|
||||
this._queuedItems.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
this.retractedItems.delete(itemID);
|
||||
var items = [...this._queuedItems];
|
||||
var prefixStrings = [...this._queuedPrefixStrings];
|
||||
this._queuedItems.clear();
|
||||
this._queuedPrefixStrings.clear();
|
||||
var addedItems = [];
|
||||
try {
|
||||
addedItems = await this._downloadPossibleMatches(prefixStrings);
|
||||
}
|
||||
catch (e) {
|
||||
// Add back to queue on failure
|
||||
for (let item of items) {
|
||||
this._queuedItems.add(item);
|
||||
}
|
||||
for (let prefixStr of prefixStrings) {
|
||||
this._queuedPrefixStrings.add(prefixStr);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
await Zotero.Notifier.trigger('refresh', 'item', [itemID]);
|
||||
},
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}, 1000),
|
||||
|
||||
updateFromServer: async function () {
|
||||
var urlPrefix = (Zotero.Prefs.get("api.url") || ZOTERO_CONFIG.API_URL) + '/retractions/';
|
||||
|
||||
// Download list
|
||||
var headers = {};
|
||||
if (this._cacheETag) {
|
||||
|
@ -256,7 +186,7 @@ Zotero.Retractions = {
|
|||
}
|
||||
var req = await Zotero.HTTP.request(
|
||||
"GET",
|
||||
urlPrefix + 'list',
|
||||
this._getURLPrefix() + 'list',
|
||||
{
|
||||
headers,
|
||||
noCache: true,
|
||||
|
@ -309,17 +239,77 @@ Zotero.Retractions = {
|
|||
if (!prefixesToSend.size) {
|
||||
Zotero.debug("No possible retractions");
|
||||
await Zotero.DB.queryAsync("DELETE FROM retractedItems");
|
||||
this.retractedItems.clear();
|
||||
this._retractedItems.clear();
|
||||
await this._saveCacheFile(list, etag, doiPrefixLength, pmidPrefixLength);
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Diff list
|
||||
|
||||
await this.downloadPossibleMatches([...prefixesToSend]);
|
||||
await this._downloadPossibleMatches([...prefixesToSend]);
|
||||
await this._cacheList(list, etag, doiPrefixLength, pmidPrefixLength);
|
||||
},
|
||||
|
||||
/**
|
||||
* @return {Number[]} - Array of added item ids
|
||||
*/
|
||||
_downloadPossibleMatches: async function (prefixStrings) {
|
||||
var req = await Zotero.HTTP.request(
|
||||
"POST",
|
||||
this._getURLPrefix() + 'search',
|
||||
{
|
||||
body: JSON.stringify(prefixStrings),
|
||||
responseType: 'json'
|
||||
}
|
||||
);
|
||||
var results = req.response;
|
||||
Zotero.debug(`Retrieved ${results.length} possible `
|
||||
+ Zotero.Utilities.pluralize(results.length, ['match', 'matches']));
|
||||
|
||||
// TODO: Prompt
|
||||
// Look in the key mappings for local items that match and add them as retractions
|
||||
var addedItemIDs = new Set();
|
||||
for (let row of results) {
|
||||
if (row.doi) {
|
||||
let ids = this._keyItems[this.TYPE_DOI].get(row.doi);
|
||||
if (ids) {
|
||||
for (let id of ids) {
|
||||
addedItemIDs.add(id);
|
||||
await this._addEntry(id, row);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (row.pmid) {
|
||||
let ids = this._keyItems[this.TYPE_PMID].get(row.pmid.toString());
|
||||
if (ids) {
|
||||
for (let id of ids) {
|
||||
addedItemIDs.add(id);
|
||||
await this._addEntry(id, row);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Zotero.debug(`Found ${addedItemIDs.size} retracted `
|
||||
+ Zotero.Utilities.pluralize(addedItemIDs.size, 'item'));
|
||||
addedItemIDs = [...addedItemIDs];
|
||||
if (addedItemIDs.length) {
|
||||
this._showAlert(addedItemIDs); // async
|
||||
}
|
||||
return addedItemIDs;
|
||||
},
|
||||
|
||||
_showAlert: async function (itemIDs) {
|
||||
// Don't show banner for items in the trash
|
||||
var items = await Zotero.Items.getAsync(itemIDs);
|
||||
items = items.filter(item => !item.deleted);
|
||||
if (!items.length) {
|
||||
return;
|
||||
}
|
||||
Zotero.Prefs.set('retractions.recentItems', JSON.stringify(items.map(item => item.id)));
|
||||
var zp = Zotero.getActiveZoteroPane();
|
||||
if (zp) {
|
||||
await zp.showRetractionBanner();
|
||||
}
|
||||
},
|
||||
|
||||
_getItemDOI: function (item) {
|
||||
|
@ -341,9 +331,8 @@ Zotero.Retractions = {
|
|||
return false;
|
||||
}
|
||||
var lines = str.split(/\n+/g);
|
||||
var fields = new Map();
|
||||
for (let line of lines) {
|
||||
let parts = line.match(/^([a-z \-]+):(.+)/i);
|
||||
let parts = line.match(/^([a-z -]+):(.+)/i);
|
||||
if (!parts) {
|
||||
continue;
|
||||
}
|
||||
|
@ -379,12 +368,12 @@ Zotero.Retractions = {
|
|||
return value.substr(0, length);
|
||||
},
|
||||
|
||||
_cacheItems: async function () {
|
||||
await this._cacheDOIItems();
|
||||
await this._cachePMIDItems();
|
||||
_cacheKeyMappings: async function () {
|
||||
await this._cacheDOIMappings();
|
||||
await this._cachePMIDMappings();
|
||||
},
|
||||
|
||||
_cacheDOIItems: async function () {
|
||||
_cacheDOIMappings: async function () {
|
||||
this._keyItems[this.TYPE_DOI] = new Map();
|
||||
this._itemKeys[this.TYPE_DOI] = new Map();
|
||||
|
||||
|
@ -411,7 +400,7 @@ Zotero.Retractions = {
|
|||
}
|
||||
},
|
||||
|
||||
_cachePMIDItems: async function () {
|
||||
_cachePMIDMappings: async function () {
|
||||
this._keyItems[this.TYPE_PMID] = new Map();
|
||||
this._itemKeys[this.TYPE_PMID] = new Map();
|
||||
|
||||
|
@ -483,6 +472,35 @@ Zotero.Retractions = {
|
|||
this.checkQueuedItems();
|
||||
},
|
||||
|
||||
_addEntry: async function (itemID, data) {
|
||||
var o = {};
|
||||
Object.assign(o, data);
|
||||
// Replace original ids with retraction ids
|
||||
if (data.retractionDOI) o.doi = data.retractionDOI;
|
||||
if (data.retractionPMID) o.pmid = data.retractionPMID;
|
||||
delete o.retractionDOI;
|
||||
delete o.retractionPMID;
|
||||
|
||||
var sql = "REPLACE INTO retractedItems VALUES (?, ?)";
|
||||
await Zotero.DB.queryAsync(sql, [itemID, JSON.stringify(o)]);
|
||||
|
||||
this._retractedItems.add(itemID);
|
||||
|
||||
await Zotero.Notifier.trigger('refresh', 'item', [itemID]);
|
||||
},
|
||||
|
||||
_removeEntry: async function (itemID) {
|
||||
this._deleteItemKeyMappings(itemID);
|
||||
|
||||
if (this._retractedItems.has(itemID)) {
|
||||
await Zotero.DB.queryAsync("DELETE FROM retractedItems WHERE itemID=?", itemID);
|
||||
}
|
||||
|
||||
this._retractedItems.delete(itemID);
|
||||
|
||||
await Zotero.Notifier.trigger('refresh', 'item', [itemID]);
|
||||
},
|
||||
|
||||
_loadCacheFile: async function () {
|
||||
if (!await OS.File.exists(this._cacheFile)) {
|
||||
return;
|
||||
|
@ -523,6 +541,15 @@ Zotero.Retractions = {
|
|||
}
|
||||
},
|
||||
|
||||
_getURLPrefix: function () {
|
||||
var url = (Zotero.Prefs.get("api.url") || ZOTERO_CONFIG.API_URL);
|
||||
if (!url.endsWith('/')) {
|
||||
url += '/';
|
||||
}
|
||||
url += 'retractions/';
|
||||
return url;
|
||||
},
|
||||
|
||||
// https://retractionwatch.com/retraction-watch-database-user-guide/retraction-watch-database-user-guide-appendix-b-reasons/
|
||||
_reasonDescriptions: {
|
||||
"Author Unresponsive": "Author(s) lack of communication after prior contact by Journal, Publisher or other original Authors",
|
||||
|
|
|
@ -241,6 +241,10 @@ var ZoteroPane = new function()
|
|||
}, 0);
|
||||
}
|
||||
|
||||
setTimeout(function () {
|
||||
ZoteroPane.showRetractionBanner();
|
||||
});
|
||||
|
||||
// TEMP: Clean up extra files from Mendeley imports <5.0.51
|
||||
setTimeout(async function () {
|
||||
var needsCleanup = await Zotero.DB.valueQueryAsync(
|
||||
|
@ -4728,6 +4732,55 @@ var ZoteroPane = new function()
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a retraction banner if there are retracted items that we haven't warned about
|
||||
*/
|
||||
this.showRetractionBanner = async function (items) {
|
||||
var items;
|
||||
try {
|
||||
items = JSON.parse(Zotero.Prefs.get('retractions.recentItems'));
|
||||
}
|
||||
catch (e) {
|
||||
Zotero.Prefs.clear('retractions.recentItems');
|
||||
Zotero.logError(e);
|
||||
return;
|
||||
}
|
||||
if (!items.length) {
|
||||
return;
|
||||
}
|
||||
items = await Zotero.Items.getAsync(items);
|
||||
if (!items.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('retracted-items-container').removeAttribute('collapsed');
|
||||
|
||||
var message = document.getElementById('retracted-items-message');
|
||||
var link = document.getElementById('retracted-items-link');
|
||||
var close = document.getElementById('retracted-items-close');
|
||||
|
||||
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);
|
||||
}.bind(this);
|
||||
|
||||
close.onclick = function () {
|
||||
this.hideRetractionBanner();
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
|
||||
this.hideRetractionBanner = function () {
|
||||
document.getElementById('retracted-items-container').setAttribute('collapsed', true);
|
||||
Zotero.Prefs.clear('retractions.recentItems');
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets the layout to either a three-vertical-pane layout and a layout where itemsPane is above itemPane
|
||||
*/
|
||||
|
|
|
@ -215,6 +215,14 @@
|
|||
</hbox>
|
||||
</toolbar>
|
||||
|
||||
<vbox id="retracted-items-container" collapsed="true">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" id="retracted-items-banner" collapsed="true">
|
||||
<div id="retracted-items-message"/>
|
||||
<div id="retracted-items-link"/>
|
||||
<div id="retracted-items-close">×</div>
|
||||
</div>
|
||||
</vbox>
|
||||
|
||||
<popupset>
|
||||
<menupopup id="zotero-collectionmenu"
|
||||
oncommand="ZoteroPane.onCollectionContextMenuSelect(event)">
|
||||
|
@ -279,7 +287,6 @@
|
|||
</menupopup>
|
||||
</popupset>
|
||||
|
||||
|
||||
<hbox id="zotero-trees" flex="1">
|
||||
<vbox id="zotero-collections-pane" zotero-persist="width">
|
||||
<!-- This is used for positioning the sync error icon panel
|
||||
|
|
|
@ -1199,8 +1199,12 @@ licenses.cc-by-nc = Creative Commons Attribution-NonCommercial 4.0 International
|
|||
licenses.cc-by-nc-nd = Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License
|
||||
licenses.cc-by-nc-sa = Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License
|
||||
|
||||
retraction.alert.single = An item in your database has been retracted.
|
||||
retraction.alert.multiple = Items in your database have been retracted.
|
||||
retraction.alert.view.single = View Retracted Item
|
||||
retraction.alert.view.multiple = View Retracted Items
|
||||
retraction.banner = This work has been retracted.
|
||||
retraction.date = Retracted on %S
|
||||
retraction.notice = Retraction Notice
|
||||
retraction.details = More details:
|
||||
retraction.credit = Data from %S
|
||||
retraction.credit = Data from %S
|
|
@ -47,7 +47,7 @@
|
|||
|
||||
#retraction-banner {
|
||||
padding: 1.5em 1em;
|
||||
background: #e02a20;
|
||||
background: #ea3232;
|
||||
width: 100%;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
|
|
|
@ -729,6 +729,41 @@
|
|||
margin-left: 3px !important;
|
||||
}
|
||||
|
||||
#retracted-items-banner {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
background: #ea3232;
|
||||
line-height: 2.5em;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
padding: 0 2em;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#retracted-items-message {
|
||||
margin-right: .8em;
|
||||
}
|
||||
|
||||
#retracted-items-link {
|
||||
text-decoration: underline;
|
||||
margin-left: .3em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#retracted-items-link:active {
|
||||
color: #f9e8e2;
|
||||
}
|
||||
|
||||
#retracted-items-close {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 3px;
|
||||
right: 9px;
|
||||
font-size: 28px;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
/* BEGIN 2X BLOCK -- DO NOT EDIT MANUALLY -- USE 2XIZE */
|
||||
@media (min-resolution: 1.25dppx) {
|
||||
|
|
|
@ -191,3 +191,5 @@ pref("extensions.zotero.translators.attachSupplementary", false);
|
|||
pref("extensions.zotero.translators.supplementaryAsLink", false);
|
||||
pref("extensions.zotero.translators.RIS.import.ignoreUnknown", true);
|
||||
pref("extensions.zotero.translators.RIS.import.keepID", false);
|
||||
|
||||
pref("extensions.zotero.retractions.recentItems", "[]");
|
||||
|
|
39
test/tests/retractionsTest.js
Normal file
39
test/tests/retractionsTest.js
Normal file
|
@ -0,0 +1,39 @@
|
|||
describe("Retractions", function() {
|
||||
describe("Notification Banner", function () {
|
||||
var win;
|
||||
var zp;
|
||||
|
||||
before(async function () {
|
||||
win = await loadZoteroPane();
|
||||
zp = win.ZoteroPane;
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
win.document.getElementById('retracted-items-close').click();
|
||||
});
|
||||
|
||||
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, {});
|
||||
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();
|
||||
|
||||
while (zp.collectionsView.selectedTreeRow.id != 'L1') {
|
||||
await Zotero.Promise.delay(10);
|
||||
}
|
||||
await waitForItemsLoad(win);
|
||||
|
||||
var item = await zp.getSelectedItems()[0];
|
||||
assert.equal(item, item2);
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue