diff --git a/frontend/src/components/Channels.tsx b/frontend/src/components/Channels.tsx index 82718126..0f1f7fe9 100644 --- a/frontend/src/components/Channels.tsx +++ b/frontend/src/components/Channels.tsx @@ -209,36 +209,41 @@ export default function Channels() { notifyYouTubeActionError(e, t("channels.notify.fullHistoryFailed"), onOpenWizard), }); - // Channel-name search (from the shared top SearchBar, via props) + tag-chip filtering, applied - // client-side over the status-filtered list. - const [tagFilter, setTagFilter] = useState([]); + // The tag-chip row is a controlled view over the Tags COLUMN filter (`filters.tags`, a string[] of + // tag ids), the same way the sync chips drive `filters.sync` — so the top chips and the Tags column + // funnel share ONE state and stay in sync (bidirectional). Toggling a chip rewrites `filters.tags`; + // filterRows / DataTable then applies it. Channel-name search stays separate (the shared top box). + const tagFilter = Array.isArray(filters.tags) ? filters.tags : []; + const toggleTagFilter = (tagId: number) => { + const id = String(tagId); + const next = tagFilter.includes(id) ? tagFilter.filter((x) => x !== id) : [...tagFilter, id]; + setFilters({ ...filters, tags: next }); + }; // A focus-channel intent (header "without full history" / tag manager) seeds the search box. useEffect(() => { if (focusChannelName) setSearch(focusChannelName); // eslint-disable-next-line react-hooks/exhaustive-deps }, [focusChannelToken]); - // The header's reset intent clears the search + tag chips (skip the initial mount). + // The header's reset intent clears the search box (skip the initial mount); the tag chips ride in + // `filters.tags`, which goToFullHistory already clears by setting a fresh map. const resetRef = useRef(filtersResetToken); useEffect(() => { if (filtersResetToken === resetRef.current) return; resetRef.current = filtersResetToken; setSearch(""); - setTagFilter([]); }, [filtersResetToken]); - // The sync-status filter is now ONE controlled column filter (`filters.sync`) shared by the top - // chips, the Sync column funnel, and the card layout — no separate pre-filter. Hidden channels have - // their own tab, so they're excluded from the subscribed table (no more dimmed rows). The name - // search + tag chips still filter here; the column filters (sync + priority/tags/text funnels) are - // applied by DataTable in table mode and by filterRows below in card mode, over the same `columns`. + // The sync-status AND tag filters are now controlled column filters (`filters.sync` / `filters.tags`) + // shared by the top chips, the column funnels, and the card layout — no separate pre-filter. Hidden + // channels have their own tab, so they're excluded from the subscribed table (no more dimmed rows). + // Only the name search filters here; the column filters (sync/tags/priority/text funnels) are applied + // by DataTable in table mode and by filterRows below in card mode, over the same `columns`. const hiddenChannels = (channelsQuery.data ?? []).filter((c) => c.hidden); const channels = (channelsQuery.data ?? []).filter((c) => !c.hidden); const q = search.trim().toLowerCase(); - const visibleChannels = channels.filter((c) => { - if (q && !`${c.title ?? ""} ${c.handle ?? ""}`.toLowerCase().includes(q)) return false; - if (tagFilter.length && !tagFilter.some((id) => c.tag_ids.includes(id))) return false; - return true; - }); + const visibleChannels = channels.filter( + (c) => !q || `${c.title ?? ""} ${c.handle ?? ""}`.toLowerCase().includes(q), + ); const s = statusQuery.data; const onView = (c: ManagedChannel) => @@ -286,7 +291,7 @@ export default function Channels() { // Status chips sit in the table's controls row (next to paging) — compact and close to // where they act. - const syncFilter = (filters.sync as string) ?? ""; + const syncFilter = typeof filters.sync === "string" ? filters.sync : ""; const statusChips = (
{SYNC_CHIPS.map((c) => ( @@ -423,7 +428,7 @@ export default function Channels() { // handled by the hook's clamp instead. useEffect(() => { resetPage(); - }, [filters, search, tagFilter, sort, resetPage]); + }, [filters, search, sort, resetPage]); const cardPager = ; const tabs = ( @@ -466,24 +471,19 @@ export default function Channels() { {t("channels.tags.yourTags")} - {userTags.map((tg) => { - const active = tagFilter.includes(tg.id); - return ( - - ); - })} + {userTags.map((tg) => ( + + ))} {tagFilter.length > 0 && (