diff --git a/chrome/content/zotero/xpcom/data/library.js b/chrome/content/zotero/xpcom/data/library.js index 225a8c3264..3d43d0704e 100644 --- a/chrome/content/zotero/xpcom/data/library.js +++ b/chrome/content/zotero/xpcom/data/library.js @@ -683,6 +683,13 @@ Zotero.Library.prototype._eraseData = async function (env) { await Zotero.DB.queryAsync("DELETE FROM libraries WHERE libraryID=?", this.libraryID); // TODO: Emit event so this doesn't have to be here await Zotero.Fulltext.clearLibraryVersion(this.libraryID); + + // Discard undo/redo history that references this library + if (Zotero.UndoHistory) { + Zotero.DB.addCurrentCallback('commit', function () { + Zotero.UndoHistory.clearForLibrary(this.libraryID); + }.bind(this)); + } }; Zotero.Library.prototype._finalizeErase = async function (env) { diff --git a/chrome/content/zotero/xpcom/undoHistory.js b/chrome/content/zotero/xpcom/undoHistory.js index 66fcfadab4..e505e3f2ac 100644 --- a/chrome/content/zotero/xpcom/undoHistory.js +++ b/chrome/content/zotero/xpcom/undoHistory.js @@ -58,6 +58,17 @@ Zotero.UndoHistory = { this._pendingEntry = null; }, + /** + * Discard both stacks if any entry references an object in the given library. + * @param {Integer} libraryID + */ + clearForLibrary(libraryID) { + let affectsLibrary = entry => entry.changes.some(change => change.libraryID === libraryID); + if (this._undoStack.some(affectsLibrary) || this._redoStack.some(affectsLibrary)) { + this.clear(); + } + }, + /** * Return a window controller for cmd_undo/cmd_redo that defers to * native text-editing controllers when they are active. diff --git a/test/tests/undoHistoryTest.js b/test/tests/undoHistoryTest.js index 273b381a10..49a1097c0e 100644 --- a/test/tests/undoHistoryTest.js +++ b/test/tests/undoHistoryTest.js @@ -281,6 +281,58 @@ describe("Zotero.UndoHistory", function () { }); }); + describe("library erasure", function () { + it("should clear undo history when a related library is erased", async function () { + let group = await createGroup(); + let collection = await createDataObject( + 'collection', { libraryID: group.libraryID, name: 'Group Collection' } + ); + Zotero.UndoHistory.clear(); + + collection.name = 'Renamed'; + await collection.saveTx({ undoAction: 'undo-action-rename-collection' }); + assert.isTrue(Zotero.UndoHistory.canUndo()); + + // The group's objects are cascade-deleted without per-object events, so + // the related undo entry must be discarded + await group.eraseTx(); + assert.isFalse(Zotero.UndoHistory.canUndo()); + assert.isFalse(Zotero.UndoHistory.canRedo()); + }); + + it("should clear undo history when the erased library is referenced only in the redo stack", async function () { + let group = await createGroup(); + let collection = await createDataObject( + 'collection', { libraryID: group.libraryID, name: 'Group Collection' } + ); + Zotero.UndoHistory.clear(); + + collection.name = 'Renamed'; + await collection.saveTx({ undoAction: 'undo-action-rename-collection' }); + // Move the entry onto the redo stack + await Zotero.UndoHistory.undo(); + assert.isTrue(Zotero.UndoHistory.canRedo()); + + await group.eraseTx(); + assert.isFalse(Zotero.UndoHistory.canRedo()); + assert.isFalse(Zotero.UndoHistory.canUndo()); + }); + + it("should preserve undo history when an unrelated library is erased", async function () { + // Record an undo entry in the user library + let collection = await createDataObject('collection', { name: 'My Library Collection' }); + Zotero.UndoHistory.clear(); + collection.name = 'Renamed'; + await collection.saveTx({ undoAction: 'undo-action-rename-collection' }); + assert.isTrue(Zotero.UndoHistory.canUndo()); + + // Erasing an unrelated group must not touch the user-library history + let group = await createGroup(); + await group.eraseTx(); + assert.isTrue(Zotero.UndoHistory.canUndo()); + }); + }); + describe("canUndo/canRedo", function () { it("should return false when stacks are empty", function () { assert.isFalse(Zotero.UndoHistory.canUndo());