From ca9dd549233b9a1b0806421022f2998f5b8bae29 Mon Sep 17 00:00:00 2001 From: npeter83 Date: Fri, 14 Aug 2026 02:19:44 +0200 Subject: [PATCH] fix(channels,datatable): address R13 S2a review + unify tag chips with the Tags column filter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review fixes (user /code-review high): - DataTable: hoist a stable EMPTY_FILTERS fallback so a controlled caller that omits `filters` doesn't churn a fresh {} each render. - DataTable: the persistKey blob now stores only the slices this table OWNS — a controlled sort/filter is written as null/{} instead of a stale never-read duplicate. - useFilterState: optional `migrate` seed (used only when storage is empty); ChannelsProvider carries the retired LS.channelFilter sync chip forward via migrateSyncChip. - Channels: guard the sync-chip read with typeof instead of an `as string` cast. Also unify the tag filter, same as the sync one: the top "YOUR TAGS" chip row is now a controlled view over the Tags COLUMN filter (`filters.tags`), replacing its own tagFilter state — so the chips and the Tags funnel share one state bidirectionally, the card layout applies it via filterRows, and it persists with the rest of the map. --- frontend/src/components/Channels.tsx | 66 ++++++++++---------- frontend/src/components/ChannelsProvider.tsx | 12 +++- frontend/src/components/DataTable.tsx | 22 ++++++- frontend/src/lib/storage.ts | 3 + frontend/src/lib/useFilterState.dom.test.ts | 15 +++++ frontend/src/lib/useFilterState.ts | 14 ++++- 6 files changed, 93 insertions(+), 39 deletions(-) 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 && (