From e88b7ee10f99fe98843220b28aff83e101b29460 Mon Sep 17 00:00:00 2001 From: Dan Stillman Date: Fri, 4 Sep 2026 15:07:15 -0400 Subject: [PATCH] Don't clear the hidden flag when refreshing retractions data _addEntry() wrote the row with REPLACE INTO, listing only itemID and data, so flag reverted to its default of 0. It's called for every match on every update, not just new ones, so a retraction the user had hidden came back as soon as the list changed and Zotero restarted. --- chrome/content/zotero/xpcom/retractions.js | 4 +- test/tests/retractionsTest.js | 54 ++++++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/chrome/content/zotero/xpcom/retractions.js b/chrome/content/zotero/xpcom/retractions.js index 32e4d561b9..a6de86184f 100644 --- a/chrome/content/zotero/xpcom/retractions.js +++ b/chrome/content/zotero/xpcom/retractions.js @@ -911,7 +911,9 @@ Zotero.Retractions = { delete o.retractionDOI; delete o.retractionPMID; - var sql = "REPLACE INTO retractedItems (itemID, data) VALUES (?, ?)"; + // Leave the flag alone, so a hidden retraction stays hidden across refreshes + var sql = "INSERT INTO retractedItems (itemID, data) VALUES (?, ?) " + + "ON CONFLICT(itemID) DO UPDATE SET data=excluded.data"; await Zotero.DB.queryAsync(sql, [itemID, JSON.stringify(o)]); var item = await Zotero.Items.getAsync(itemID); diff --git a/test/tests/retractionsTest.js b/test/tests/retractionsTest.js index a575bd54d7..c3b0a61abc 100644 --- a/test/tests/retractionsTest.js +++ b/test/tests/retractionsTest.js @@ -257,6 +257,60 @@ describe("Retractions", function () { Zotero.Retractions._version = version; } }); + + it("should keep a hidden retraction hidden across an update", async function () { + var doi = '10.1234/defgh'; + var hash = Zotero.Utilities.Internal.sha1(doi); + var line = Zotero.Retractions.TYPE_DOI + hash.substr(0, 5) + ' 12345\n'; + var etags = ['first', 'second']; + + server.respond(function (req) { + if (req.method == 'GET' && req.url == baseURL + 'list') { + req.respond( + 200, + { 'Content-Type': 'text/plain', 'ETag': etags.shift() || 'second' }, + line + ); + } + else if (req.method == 'POST' && req.url == baseURL + 'search') { + req.respond( + 200, + { 'Content-Type': 'application/json' }, + JSON.stringify([ + { + doi: hash, + retractionDOI: '10.1234/efghi', + date: '2019-01-02', + reasons: ["Error in Data"], + urls: [] + } + ]) + ); + } + }); + + await Zotero.Retractions.updateFromServer(); + + let promise = waitForItemEvent('refresh'); + let item = createUnsavedDataObject('item', { itemType: 'journalArticle' }); + item.setField('DOI', doi); + await item.saveTx(); + await promise; + assert.isTrue(Zotero.Retractions.isRetracted(item)); + + await Zotero.Retractions.hideRetraction(item); + assert.isFalse(Zotero.Retractions.isRetracted(item)); + + // The stored flag is what survives a restart, so it has to outlast an update + await Zotero.Retractions.updateFromServer(); + assert.isFalse(Zotero.Retractions.isRetracted(item)); + assert.equal( + await Zotero.DB.valueQueryAsync( + "SELECT flag FROM retractedItems WHERE itemID=?", item.id + ), + Zotero.Retractions.FLAG_HIDDEN + ); + }); });