diff --git a/chrome/content/zotero/integration/citationDialog/searchHandler.mjs b/chrome/content/zotero/integration/citationDialog/searchHandler.mjs index a7cfdc11aa..ce4d37c487 100644 --- a/chrome/content/zotero/integration/citationDialog/searchHandler.mjs +++ b/chrome/content/zotero/integration/citationDialog/searchHandler.mjs @@ -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; diff --git a/test/tests/citationDialogTest.js b/test/tests/citationDialogTest.js index d2c3093023..37b758caf7 100644 --- a/test/tests/citationDialogTest.js +++ b/test/tests/citationDialogTest.js @@ -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 () {