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
+39 -1
View File
@@ -1,6 +1,6 @@
import type { ComponentProps, ReactNode } from "react";
import type { TFunction } from "i18next";
import { NO_TAG_SENTINEL, type ManagedChannel } from "../lib/api";
import { NO_TAG_SENTINEL, type ManagedChannel, type Tag } from "../lib/api";
import { formatCountOrDash, formatTotalHours } from "../lib/format";
import type { Column } from "./ui/DataTable";
import { PriorityStepper } from "./ui/PriorityStepper";
@@ -10,6 +10,7 @@ import { ChannelTagsControl, type SuggestedTag } from "./ChannelTags";
import { StoredCell } from "./channelCells";
import { isFullySynced } from "./channelSync";
import { RelativeTime } from "./ui/RelativeTime";
import type { FilterChip } from "./TagFilterChips";
/** The shared "subscribers" DataTable column (right-aligned, sortable, em-dash for null) — identical
* in the Channels manager and the ChannelDiscovery table, which show subscriber counts on different
@@ -49,6 +50,43 @@ export function tagsColumnTest(c: ManagedChannel, values: string[]): boolean {
return values.some((v) => c.tag_ids.includes(Number(v)));
}
/** The manager's tag-filter chip row (the Tags column filter's values): any picked id whose tag no
* longer exists, then each user tag, then the "No tag" sentinel. A stored pick can outlive its tag
* (deleted here or on another device) and still narrows the table to nothing, so it stays a visible,
* removable chip — FIRST, so the row's "+N" overflow never folds the reason away (E32 S2). */
export function tagFilterChips(o: {
userTags: Tag[];
allTags: Tag[] | undefined;
picked: string[];
noTagOn: boolean;
toggle: (tagId: number) => void;
toggleNoTag: () => void;
t: TFunction;
}): FilterChip[] {
const known = new Set((o.allTags ?? []).map((tg) => String(tg.id)));
const gone = o.allTags ? o.picked.filter((id) => id !== NO_TAG_SENTINEL && !known.has(id)) : [];
return [
...gone.map((id) => ({
key: `t${id}`,
label: o.t("channels.tags.deleted"),
active: true,
onToggle: () => o.toggle(Number(id)),
})),
...o.userTags.map((tg) => ({
key: `t${tg.id}`,
label: tg.name,
active: o.picked.includes(String(tg.id)),
onToggle: () => o.toggle(tg.id),
})),
{
key: NO_TAG_SENTINEL,
label: o.t("channels.tags.noTag"),
active: o.noTagOn,
onToggle: o.toggleNoTag,
},
];
}
/** Everything the Channels-manager column set closes over. The manager owns the state + mutations;
* this factory just wires them into `Column<ManagedChannel>` render/filter/sort defs so the ~150-line
* column block lives outside the page component (its cell components are in channelCells). */