fix(channels,datatable): address R13 S2a review + unify tag chips with the Tags column filter

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.
This commit is contained in:
2026-08-14 02:19:44 +02:00
parent 837fdcd408
commit ca9dd54923
6 changed files with 93 additions and 39 deletions
+11 -1
View File
@@ -33,6 +33,16 @@ function clampView(v: string | null): ChannelsView {
return v === "discovery" || v === "blocked" || v === "hidden" ? v : "subscribed";
}
// One-time carry-forward of the retired single-value sync chip (LS.legacyChannelFilter, vocab
// all/needs_full/fully_synced) into the new filter map's `sync` key (vocab ""/needsFull/fully), so a
// user who had a chip pinned keeps it. Only consulted when channelsFilterMap is still empty.
function migrateSyncChip(): FilterMap | null {
const old = getAccountRaw(LS.legacyChannelFilter);
if (old === "needs_full") return { sync: "needsFull" };
if (old === "fully_synced") return { sync: "fully" };
return null;
}
type ChannelsState = {
search: string;
// The subscribed table's column filters, lifted out of DataTable so the top status chips, the Sync
@@ -105,7 +115,7 @@ export function ChannelsProvider({ children }: { children: ReactNode }) {
const [search, setSearchState] = useState("");
// The subscribed table's column-filter map, owned here (lifted out of DataTable) so the top status
// chips, the Sync column funnel, and the card layout share one state; useFilterState persists it.
const [filters, setFilters] = useFilterState(LS.channelsFilterMap);
const [filters, setFilters] = useFilterState(LS.channelsFilterMap, migrateSyncChip);
const [view, setViewState] = useState<ChannelsView>(() =>
clampView(getAccountRaw(LS.channelsView)),
);