From a1292005ceeea44a725947dedf0ed9464bd5c6f0 Mon Sep 17 00:00:00 2001 From: lrnselfreliance <33263708+lrnselfreliance@users.noreply.github.com> Date: Wed, 7 Oct 2026 18:22:05 -0600 Subject: [PATCH] Other search tab: search Channels by name, add Domains and Playlists, fix the estimate (#785) * Count Channels with any of the Tags in the Other search estimate The Other tab lists Channels with any of the selected Tags, but its estimate only counted Channels with the first Tag, so the two disagreed when several Tags were selected. Co-Authored-By: Claude Opus 5.5 * Search Channels by name in the Other search tab The Other tab only listed Channels with the selected Tags, so typing a Channel's name found nothing. Channels now match the search text by name, and the Tags when there are both. The list and its estimate share one query. Co-Authored-By: Claude Opus 5.5 * Add Domains to the Other search tab Domains match the search text by name and/or any of the Tags, like Channels. The list and its estimate share one query. Co-Authored-By: Claude Opus 5.5 * Add Playlists to the Other search tab Domains and Playlists are Collections, so the Other tab searches both with the existing /api/collections/search (by kind) instead of a Domain-only endpoint. The search and the estimate share search_collections_query. The search now loads each Collection's Downloads up front instead of once per Collection. Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 --- app/src/api.js | 14 ++++- app/src/components/OtherSearchView.test.js | 57 ++++++++++++++++++- app/src/components/Search.js | 36 +++++++++++- app/src/hooks/customHooks.js | 35 +++++++++++- app/src/hooks/customHooks.test.js | 20 ++++++- modules/repos/test/test_search.py | 6 +- modules/videos/channel/api.py | 2 +- modules/videos/channel/lib.py | 33 ++++++++--- modules/videos/channel/test/test_api.py | 29 ++++++++++ modules/videos/schema.py | 1 + wrolpi/collections/lib.py | 55 ++++++++---------- .../test/test_collection_search.py | 47 +++++++++++++++ wrolpi/common.py | 35 +++++------- wrolpi/test/test_root_api.py | 12 ++++ 14 files changed, 309 insertions(+), 73 deletions(-) create mode 100644 wrolpi/collections/test/test_collection_search.py diff --git a/app/src/api.js b/app/src/api.js index 142f7ec3..8b2a421b 100644 --- a/app/src/api.js +++ b/app/src/api.js @@ -2118,8 +2118,8 @@ export async function searchEstimateOthers(tagNames, searchStr) { } } -export async function searchChannels(tagNames) { - const body = {tag_names: tagNames}; +export async function searchChannels(tagNames, searchStr) { + const body = {tag_names: tagNames, search_str: searchStr}; const response = await apiPost(`${API_URI}/videos/channels/search`, body); if (response.ok) { const content = await response.json(); @@ -2129,6 +2129,16 @@ export async function searchChannels(tagNames) { } } +// Collections of `kind` whose name matches `searchStr` and/or which have any of the tags. +export async function searchCollections(kind, tagNames, searchStr) { + const body = {kind, tag_names: tagNames, search_str: searchStr}; + const response = await apiPost(`${COLLECTIONS_API}/search`, body); + if (response.ok) { + const content = await response.json(); + return {collections: content.collections}; + } +} + export async function getOutdatedZims() { const response = await apiGet(`${API_URI}/zim/outdated`); if (response.status === 200) { diff --git a/app/src/components/OtherSearchView.test.js b/app/src/components/OtherSearchView.test.js index e2526ec3..c4de3dc9 100644 --- a/app/src/components/OtherSearchView.test.js +++ b/app/src/components/OtherSearchView.test.js @@ -1,7 +1,7 @@ import React from 'react'; import {act, render, screen} from '../test-utils'; import {OtherSearchView} from './Search'; -import {searchChannels, searchRepos} from '../api'; +import {searchChannels, searchCollections, searchRepos} from '../api'; // What the search page's Other tab shows for each state of useSearchChannels. "No Channels" // must only appear once the server has confirmed there are none. @@ -9,6 +9,7 @@ import {searchChannels, searchRepos} from '../api'; jest.mock('../api', () => ({ ...jest.requireActual('../api'), searchChannels: jest.fn(), + searchCollections: jest.fn(), searchRepos: jest.fn(), })); @@ -18,6 +19,8 @@ beforeEach(() => { searchChannels.mockReset(); searchRepos.mockReset(); searchRepos.mockResolvedValue({repos: [], total: 0}); + searchCollections.mockReset(); + searchCollections.mockResolvedValue({collections: []}); }); test('pending: shows a loader, not "No Channels"', () => { @@ -117,3 +120,55 @@ describe('Repos', () => { expect(screen.getByText(/files