From 6bbcaba7ff7732cfbca43a92a72d937c87d55796 Mon Sep 17 00:00:00 2001 From: Dan Stillman Date: Tue, 16 Jun 2026 15:54:57 -0400 Subject: [PATCH] Use one combined tag query for a multiple-row selection --- .../containers/tagSelectorContainer.jsx | 9 +++-- .../content/zotero/xpcom/collectionTreeRow.js | 34 +++++++++++++++++++ 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/chrome/content/zotero/containers/tagSelectorContainer.jsx b/chrome/content/zotero/containers/tagSelectorContainer.jsx index 5f948bb209..a1a2cfc382 100644 --- a/chrome/content/zotero/containers/tagSelectorContainer.jsx +++ b/chrome/content/zotero/containers/tagSelectorContainer.jsx @@ -130,9 +130,12 @@ Zotero.TagSelector = class TagSelectorContainer extends React.PureComponent { */ async _safeGetTags(...args) { try { - let tags = (await Promise.all( - this.collectionTreeRows.map(row => row.getTags(...args)) - )).flat(); + let rows = this.collectionTreeRows || []; + // A single row uses its own cached temp table directly; multiple rows are + // combined into one temp table and query to avoid a per-row query + let tags = rows.length == 1 + ? await rows[0].getTags(...args) + : await Zotero.CollectionTreeRow.getTagsAcrossRows(rows, ...args); // Multiple rows (collections, or collections across libraries) can return // the same tag, so dedupe by name return this._dedupeTags(tags); diff --git a/chrome/content/zotero/xpcom/collectionTreeRow.js b/chrome/content/zotero/xpcom/collectionTreeRow.js index b7595b2fbb..d47a7449ed 100644 --- a/chrome/content/zotero/xpcom/collectionTreeRow.js +++ b/chrome/content/zotero/xpcom/collectionTreeRow.js @@ -567,6 +567,40 @@ Zotero.CollectionTreeRow.prototype.getTags = async function (types, tagIDs) { }; +/** + * Returns all the tags used by items across multiple rows' views + * + * Combines the rows' search results into a single temporary table and runs one tag + * query, rather than creating a temp table and running a separate query per row. + * + * @param {Zotero.CollectionTreeRow[]} rows + * @param {Number[]} [types] + * @param {Number[]} [tagIDs] + * @return {Promise} + */ +Zotero.CollectionTreeRow.getTagsAcrossRows = async function (rows, types, tagIDs) { + // share/bucket/feeds rows never contribute tags (see getTags()) + var tagRows = rows.filter(row => !['share', 'bucket', 'feeds'].includes(row.type)); + if (!tagRows.length) { + return []; + } + // Combine the rows' search results into a single set of item IDs + var itemIDs = new Set(); + for (let ids of await Promise.all(tagRows.map(row => row.getSearchResults(false)))) { + for (let id of ids) { + itemIDs.add(id); + } + } + var tmpTable = await Zotero.Search.idsToTempTable([...itemIDs]); + try { + return await Zotero.Tags.getAllWithin({ tmpTable, types, tagIDs }); + } + finally { + await Zotero.DB.queryAsync(`DROP TABLE IF EXISTS ${tmpTable}`, false, { noCache: true }); + } +}; + + /** * Clear the per-instance search cache. Call this at the start of a refresh cycle * or when search/tag filters change, so the next getSearchResults()/getSearchObject()