Update items-in-view count when rows change without a selection

Notifier events that added or removed rows (e.g., items downloaded
during a sync) didn't update the no-selection message in the item pane,
so the count went stale until the selection changed. The row provider
now emits a rowCountChange event, and the pane re-renders the count,
debounced, when nothing is selected.
This commit is contained in:
Dan Stillman 2026-08-09 12:05:55 -04:00
parent b8cbdc8c95
commit fe35127f50
3 changed files with 73 additions and 1 deletions

View file

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

View file

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

View file

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