fix(feed): a filter on a deleted tag matches nothing and its chip says so

A saved view or the stored filter outlives its tag. feedquery dropped the unknown id, so the
view showed the whole feed under its old name. The id now lands in a MISSING_TAG_CATEGORY that
filters to nothing in the SQL and in feed_summary's mirror alike; the feed chip reads "Deleted
tag" (a placeholder while the tag list loads, the id if it failed). Channels' persisted tag
filter gets the same chip, first in the row so the +N overflow cannot fold it away.
This commit is contained in:
2026-10-03 21:53:05 +02:00
parent 52f12e5331
commit 8f1208fdfe
11 changed files with 190 additions and 26 deletions
+14 -19
View File
@@ -16,7 +16,7 @@ import { api, NO_TAG_SENTINEL, type ManagedChannel } from "../lib/api";
import { Sparkles } from "lucide-react";
import { notifyYouTubeActionError } from "../lib/youtubeErrors";
import { accountKey, LS } from "../lib/storage";
import { buildChannelColumns, applySyncFilter } from "./channelColumns";
import { buildChannelColumns, applySyncFilter, tagFilterChips } from "./channelColumns";
import { notify } from "../lib/notifications";
import { Badge } from "./ui/Badge";
import { Button } from "./ui/Button";
@@ -39,7 +39,7 @@ import { focusNavItem } from "../lib/listNav";
import ViewSwitcher, { type ViewOption } from "./ViewSwitcher";
import ColumnSortControl from "./ui/ColumnSortControl";
import FilterChipGroup from "./ui/FilterChipGroup";
import TagFilterChips, { type FilterChip } from "./TagFilterChips";
import TagFilterChips from "./TagFilterChips";
import ActionMenu, { type ActionMenuItem } from "./ui/ActionMenu";
import ChannelStatsPopover from "./ChannelStatsPopover";
import Pager from "./ui/Pager";
@@ -345,22 +345,17 @@ export default function Channels() {
const noTagOn = tagFilter.includes(NO_TAG_SENTINEL);
const toggleNoTagFilter = () =>
setFilters({ ...filters, tags: noTagOn ? [] : [NO_TAG_SENTINEL] });
// The tag-filter chips (user tags + the "No tag" sentinel), fed to TagFilterChips which folds any
// overflow into a "+N" pill so the row stays one line and the trailing "…" actions can't wrap.
const tagFilterChips: FilterChip[] = [
...userTags.map((tg) => ({
key: `t${tg.id}`,
label: tg.name,
active: tagFilter.includes(String(tg.id)),
onToggle: () => toggleTagFilter(tg.id),
})),
{
key: NO_TAG_SENTINEL,
label: t("channels.tags.noTag"),
active: noTagOn,
onToggle: toggleNoTagFilter,
},
];
// The tag-filter chips, fed to TagFilterChips which folds any overflow into a "+N" pill so the row
// stays one line and the trailing "…" actions can't wrap.
const tagChips = tagFilterChips({
userTags,
allTags,
picked: tagFilter,
noTagOn,
toggle: toggleTagFilter,
toggleNoTag: toggleNoTagFilter,
t,
});
// A focus-channel intent (header "without full history" / tag manager) seeds the search box.
// The TOKEN is the intent (it advances even when the same channel is re-focused); the name is the
// payload, so it is read through an effect event. Naming it would re-seed the box on any render that
@@ -735,7 +730,7 @@ export default function Channels() {
</Button>
</Tooltip>
<FilterFoldButton fold={fold} label={t("sidebar.filters")} count={foldCount} />
{!fold.folded && <TagFilterChips chips={tagFilterChips} className="flex-1" />}
{!fold.folded && <TagFilterChips chips={tagChips} className="flex-1" />}
{!fold.folded && tagFilter.length > 0 && (
<Button
variant="link-muted"