citation dlg: sort libraries by cited count (#5925)

In list mode, sort libraries by the count of their items
cited in the current document, falling back to alphabetic sorting
when counts are equal, with "My Library" always getting
priority over other groups.

Fixes: #5924
(cherry picked from commit 8f44172342)
This commit is contained in:
abaevbog 2026-05-19 12:40:55 -07:00 • committed by Dan Stillman
parent 2a1779f5c1
commit b5517e92c1
2 changed files with 81 additions and 3 deletions

View file

@ -50,6 +50,9 @@ export class CitationDialogSearchHandler {
this.selectedItems = null;
this.openItems = null;
this.citedItems = null;
// number of cited items in each library, keyed by libraryID.
// Populated once cited items are loaded in refreshCitedItems.
this.citedItemCountsByLibrary = {};
}
setSearchValue(str, enforceMinQueryLength) {
@ -84,7 +87,8 @@ export class CitationDialogSearchHandler {
// where key is selected/open/cited/{libraryID}, and group is the respective list of items.
// Groups are sorted in the order they will be rendered:
// Selected, Opened, Cited go first, followed by found library item groups ordered
// by the number of results in each library.
// by the number of cited items in each library (if loaded). Ties are broken by
// keeping My Library first, then sorting other libraries alphabetically by name.
// Items/notes in the libraries group are sorted via _createItemsSort/_createNotesSort comparators.
// Takes citedItems as a parameter to filter them out from Selected, Opened and Cited groups.
getOrderedSearchResultGroups(citedItemIDs = new Set()) {
@ -128,8 +132,18 @@ export class CitationDialogSearchHandler {
library.group.sort(itemComparator);
});
// sort libraries by the number of items
libraryItems.sort((a, b) => b.group.length - a.group.length);
// sort libraries by the number of cited items in each library;
// when counts are equal (or cited items have not been loaded yet),
// keep My Library first and sort the rest alphabetically by name
let collation = Zotero.getLocaleCollation();
libraryItems.sort((a, b) => {
let aCount = this.citedItemCountsByLibrary[a.key] || 0;
let bCount = this.citedItemCountsByLibrary[b.key] || 0;
if (aCount !== bCount) return bCount - aCount;
if (a.key === Zotero.Libraries.userLibraryID) return -1;
if (b.key === Zotero.Libraries.userLibraryID) return 1;
return collation.compareString(1, Zotero.Libraries.get(a.key).name, Zotero.Libraries.get(b.key).name);
});
result.push(...libraryItems);
return result;
@ -180,6 +194,13 @@ export class CitationDialogSearchHandler {
async refreshCitedItems() {
if (this.citedItems === null) {
this.citedItems = await this._getCitedItems();
// Record how many items from each library are already cited in the document.
// Used for sorting to move currently cited libraries to the top of search results.
for (let item of this.citedItems || []) {
let libraryID = item.libraryID;
if (!libraryID) continue;
this.citedItemCountsByLibrary[libraryID] = (this.citedItemCountsByLibrary[libraryID] || 0) + 1;
}
}
if (!this.citedItems) return;

View file

@ -571,6 +571,63 @@ describe("Citation Dialog", function () {
assert.isOk(node);
}
});
it("should sort libraries in list mode by cited count, with user library first and others alphabetical on tie", async function () {
let gammaGroup = await createGroup({ name: "gamma_group" });
let betaGroup = await createGroup({ name: "beta_group" });
let alphaGroup = await createGroup({ name: "alpha_group" });
// One search-matching item per library
let userItem = await createDataObject('item', { title: "libsort_user" });
let gammaItem = await createDataObject('item', { title: "libsort_gamma", libraryID: gammaGroup.libraryID });
let betaItem = await createDataObject('item', { title: "libsort_beta", libraryID: betaGroup.libraryID });
let alphaItem = await createDataObject('item', { title: "libsort_alpha", libraryID: alphaGroup.libraryID });
// Cited item with a title that doesn't match the search query, so it
// isn't removed from results.found by deduplication. Goes in gamma_group
// so that group has a higher cited count than the rest.
let citedItem = await createDataObject('item', { title: "cited_item", libraryID: gammaGroup.libraryID });
io.getItems = async () => [citedItem];
io.isAllCitedDataLoaded = true;
await IOManager.toggleDialogMode("list");
while (SearchHandler.searching) {
await Zotero.Promise.delay(10);
}
SearchHandler.selectedItems = [];
SearchHandler.openItems = [];
// Reset so refreshCitedItems re-fetches via io.getItems and
// populates citedItemCountsByLibrary
SearchHandler.citedItems = null;
SearchHandler.citedItemCountsByLibrary = {};
await SearchHandler.refreshCitedItems();
await dialog.currentLayout.search("libsort", { skipDebounce: true });
let libraryGroupKeys = SearchHandler.getOrderedSearchResultGroups()
.filter(g => g.isLibrary)
.map(g => g.key);
// Expected order:
// 1. gamma_group -- only library with a cited item (cited count wins)
// 2. user library -- userLibraryID wins ties over other libraries
// 3. alpha_group -- alphabetical fallback among remaining libraries
// 4. beta_group
assert.deepEqual(libraryGroupKeys, [
gammaGroup.libraryID,
userItem.libraryID,
alphaGroup.libraryID,
betaGroup.libraryID
]);
// Reset cited state so subsequent tests aren't affected
SearchHandler.citedItems = [];
SearchHandler.citedItemCountsByLibrary = {};
io.getItems = () => [];
delete io.isAllCitedDataLoaded;
});
});
describe("Dialog loading", function () {