diff --git a/chrome/content/zotero/collectionViewItemTree.jsx b/chrome/content/zotero/collectionViewItemTree.jsx index 0968d1f374..bc3f750892 100644 --- a/chrome/content/zotero/collectionViewItemTree.jsx +++ b/chrome/content/zotero/collectionViewItemTree.jsx @@ -732,6 +732,7 @@ class CollectionViewItemTreeRowProvider extends ItemTreeRowProvider { this._removeRows(rows); rowsToInvalidate = true; // all rows this.runListeners('update', true); + this.itemTree.runListeners('rowCountChange'); } } @@ -1084,6 +1085,10 @@ class CollectionViewItemTreeRowProvider extends ItemTreeRowProvider { selection: rowsToSelect }); } + + if (madeChanges || refresh) { + this.itemTree.runListeners('rowCountChange'); + } } } @@ -1102,8 +1107,10 @@ class CollectionViewItemTree extends ItemTree { // Triggered when the item tree is refreshed: // - Collection/view changed (changeCollectionTreeRow) // - Search/filter updated (setFilter) - // - Items added/removed/modified (notify -> refresh) this.onRefresh = this.createEventBinding('refresh'); + // Triggered when notifier events change the rows in the view without a full refresh + // (e.g., items added during a sync) + this.onRowCountChange = this.createEventBinding('rowCountChange'); } get viewMode() { return this.rowProvider.viewMode; } diff --git a/chrome/content/zotero/zoteroPane.js b/chrome/content/zotero/zoteroPane.js index 2bb5c836dc..3139d6e6c6 100644 --- a/chrome/content/zotero/zoteroPane.js +++ b/chrome/content/zotero/zoteroPane.js @@ -1613,6 +1613,20 @@ var ZoteroPane = new function () { ZoteroPane.itemsView.onRefresh.addListener(async () => { await ZoteroPane.itemSelected(); }); + // Also update the count when rows are added or removed by notifier events with + // nothing selected (e.g., items downloaded during a sync). Debounced so that the + // item pane doesn't re-render within the notifier dispatch or once per event in + // a burst of changes. + let updateUnselectedCount = Zotero.Utilities.debounce(() => { + if (!ZoteroPane.itemsView.selection.count) { + ZoteroPane.itemSelected(); + } + }, 100); + ZoteroPane.itemsView.onRowCountChange.addListener(() => { + if (!ZoteroPane.itemsView.selection.count) { + updateUnselectedCount(); + } + }); ZoteroPane.itemsView.waitForLoad().then(() => Zotero.uiIsReady()); ItemTreeMenuBar.setItemTreeSortKeys(ZoteroPane.itemsView); diff --git a/test/tests/itemPaneTest.js b/test/tests/itemPaneTest.js index e8011f43cb..898589124b 100644 --- a/test/tests/itemPaneTest.js +++ b/test/tests/itemPaneTest.js @@ -250,6 +250,57 @@ describe("Item pane", function () { }); }); + describe("Message pane", function () { + it("should update items-in-view count when an item is added with no selection", async function () { + var collection = await createDataObject('collection'); + await ZoteroPane.collectionsView.selectCollection(collection.id); + await waitForItemsLoad(win); + + var messageBox = doc.querySelector('#zotero-item-pane-message-box'); + var emptyMessage = await doc.l10n.formatValue('item-pane-message-unselected', { count: 0 }); + var oneItemMessage = await doc.l10n.formatValue('item-pane-message-unselected', { count: 1 }); + for (let i = 0; i < 100 && !messageBox.textContent.includes(emptyMessage); i++) { + await Zotero.Promise.delay(10); + } + assert.include(messageBox.textContent, emptyMessage); + + await createDataObject('item', { collections: [collection.id] }, { skipSelect: true }); + + for (let i = 0; i < 100 && !messageBox.textContent.includes(oneItemMessage); i++) { + await Zotero.Promise.delay(10); + } + assert.include(messageBox.textContent, oneItemMessage); + }); + + it("should update items-in-view count when a restored item is removed from the trash with no selection", async function () { + var group = await createGroup(); + var item = await createDataObject('item', { libraryID: group.libraryID, deleted: true }, { skipSelect: true }); + await selectTrash(win, group.libraryID); + + var messageBox = doc.querySelector('#zotero-item-pane-message-box'); + var emptyMessage = await doc.l10n.formatValue('item-pane-message-unselected', { count: 0 }); + var oneItemMessage = await doc.l10n.formatValue('item-pane-message-unselected', { count: 1 }); + for (let i = 0; i < 100 && !messageBox.textContent.includes(oneItemMessage); i++) { + await Zotero.Promise.delay(10); + } + assert.include(messageBox.textContent, oneItemMessage); + + // Restore the item without notifications, so that the row remains until a + // 'refresh' notification arrives (e.g., from retractions or full-text + // indexing) and the tree notices that the item is no longer deleted + item.deleted = false; + await item.saveTx({ skipNotifier: true }); + assert.include(messageBox.textContent, oneItemMessage); + + await Zotero.Notifier.trigger('refresh', 'item', [item.id]); + + for (let i = 0; i < 100 && !messageBox.textContent.includes(emptyMessage); i++) { + await Zotero.Promise.delay(10); + } + assert.include(messageBox.textContent, emptyMessage); + }); + }); + describe("Info pane", function () { before(async () => { await activateZoteroPane();