feat(channels,datatable): unify sync-status into one controlled DataTable filter (R13 S2a)
The top sync chips and the Sync column's own select filter were two independent states with different vocab (all/needs_full/fully_synced vs fully/needsFull) and subtly different semantics. DataTable gains a controlled filters/onFiltersChange pair (symmetric to the existing controlled sort); ChannelsProvider now owns the filter map via a new useFilterState hook, so the top chips, the Sync column funnel, the card layout, and the header "full history" deep-link all drive ONE filters.sync. The card layout reuses the exported filterRows predicate instead of its own status pre-filter. Vocab unified on the column's fully = recent_synced && backfill_done, which now matches the Sync cell's own "fully synced" badge.
This commit is contained in:
@@ -12,33 +12,32 @@ import { getAccountRaw, LS, setAccountRaw } from "../lib/storage";
|
||||
import { parseChannelLayout, type ChannelLayout } from "../lib/channelLayout";
|
||||
import { type SortState } from "../lib/columnSort";
|
||||
import { useSortState } from "../lib/useSortState";
|
||||
import { useFilterState } from "../lib/useFilterState";
|
||||
import type { FilterMap } from "./tableHeader";
|
||||
import { useNavigation, useNavigationActions } from "./NavigationProvider";
|
||||
import type { ChannelStatusFilter, ChannelsView } from "./Channels";
|
||||
import type { ChannelsView } from "./Channels";
|
||||
|
||||
// Owns the Channels module's shared local state — the rail-less manager's name filter, status chip,
|
||||
// active tab, the "drop a stale column filter" token, and the focus-a-channel intent — lifted out of
|
||||
// Owns the Channels module's shared local state — the rail-less manager's name filter, column-filter
|
||||
// map, active tab, the "drop a stale column filter" token, and the focus-a-channel intent — lifted out of
|
||||
// App so the Channels page, the header search box, and the cross-module callers (the feed filter
|
||||
// sidebar and the notifications inbox both "focus this channel in the manager") read it via hooks
|
||||
// instead of prop-drilling through App. Mirrors FeedFiltersProvider (split State + Actions contexts,
|
||||
// stable useCallback setters, one-line hooks; composed in main.tsx around <App/>).
|
||||
//
|
||||
// Nested INSIDE NavigationProvider so its actions can drive navigation: focusChannel and
|
||||
// goToFullHistory both jump to the Channels page (and force the subscriptions tab). status / view are
|
||||
// goToFullHistory both jump to the Channels page (and force the subscriptions tab). filters / view are
|
||||
// per-account persisted (localStorage); search / focus / the reset token are ephemeral. An account
|
||||
// switch does a full reload, so init-from-storage is all the hydration the persisted ones need.
|
||||
|
||||
const STATUS_FILTERS: readonly ChannelStatusFilter[] = ["needs_full", "fully_synced", "all"];
|
||||
|
||||
function clampStatus(v: string | null): ChannelStatusFilter {
|
||||
return STATUS_FILTERS.includes(v as ChannelStatusFilter) ? (v as ChannelStatusFilter) : "all";
|
||||
}
|
||||
function clampView(v: string | null): ChannelsView {
|
||||
return v === "discovery" || v === "blocked" || v === "hidden" ? v : "subscribed";
|
||||
}
|
||||
|
||||
type ChannelsState = {
|
||||
search: string;
|
||||
statusFilter: ChannelStatusFilter;
|
||||
// The subscribed table's column filters, lifted out of DataTable so the top status chips, the Sync
|
||||
// column funnel, and the card layout share ONE filter state (the sync chip lives at `filters.sync`).
|
||||
filters: FilterMap;
|
||||
view: ChannelsView;
|
||||
// The subscribed table's sort, lifted out of DataTable so the card layout shares it.
|
||||
sort: SortState;
|
||||
@@ -53,7 +52,7 @@ type ChannelsState = {
|
||||
|
||||
interface ChannelsActions {
|
||||
setSearch: (q: string) => void;
|
||||
setStatusFilter: (f: ChannelStatusFilter) => void;
|
||||
setFilters: (f: FilterMap) => void;
|
||||
setView: (v: ChannelsView) => void;
|
||||
setLayout: (l: ChannelLayout) => void;
|
||||
setSort: (s: SortState) => void;
|
||||
@@ -66,7 +65,7 @@ interface ChannelsActions {
|
||||
|
||||
const StateContext = createContext<ChannelsState>({
|
||||
search: "",
|
||||
statusFilter: "all",
|
||||
filters: {},
|
||||
view: "subscribed",
|
||||
sort: null,
|
||||
filtersResetToken: 0,
|
||||
@@ -79,7 +78,7 @@ const StateContext = createContext<ChannelsState>({
|
||||
const LayoutContext = createContext<ChannelLayout>("table");
|
||||
const ActionsContext = createContext<ChannelsActions>({
|
||||
setSearch: () => {},
|
||||
setStatusFilter: () => {},
|
||||
setFilters: () => {},
|
||||
setView: () => {},
|
||||
setLayout: () => {},
|
||||
setSort: () => {},
|
||||
@@ -104,9 +103,9 @@ export function ChannelsProvider({ children }: { children: ReactNode }) {
|
||||
const { setPage } = useNavigationActions();
|
||||
|
||||
const [search, setSearchState] = useState("");
|
||||
const [statusFilter, setStatusFilterState] = useState<ChannelStatusFilter>(() =>
|
||||
clampStatus(getAccountRaw(LS.channelFilter)),
|
||||
);
|
||||
// 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 [view, setViewState] = useState<ChannelsView>(() =>
|
||||
clampView(getAccountRaw(LS.channelsView)),
|
||||
);
|
||||
@@ -127,10 +126,6 @@ export function ChannelsProvider({ children }: { children: ReactNode }) {
|
||||
viewRef.current = view;
|
||||
|
||||
const setSearch = useCallback((q: string) => setSearchState(q), []);
|
||||
const setStatusFilter = useCallback((f: ChannelStatusFilter) => {
|
||||
setStatusFilterState(f);
|
||||
setAccountRaw(LS.channelFilter, f);
|
||||
}, []);
|
||||
// The active tab rides in history.state (`_chTab`), the same way the channel page (`_chan`) and the
|
||||
// live YouTube search (`_yt`) do — so Back steps back through the tabs you actually visited instead
|
||||
// of jumping straight out to the previous module.
|
||||
@@ -163,11 +158,13 @@ export function ChannelsProvider({ children }: { children: ReactNode }) {
|
||||
[setView, setPage],
|
||||
);
|
||||
const goToFullHistory = useCallback(() => {
|
||||
setStatusFilter("needs_full");
|
||||
setView("subscribed"); // the status filter applies to the subscriptions tab
|
||||
setFiltersResetToken((n) => n + 1); // drop any stale column filter hiding the rows
|
||||
// One controlled map replaces the old status chip: sync=needsFull, and dropping every other
|
||||
// column filter (the whole map is set fresh) so no stale funnel hides the rows we're sent to see.
|
||||
setFilters({ sync: "needsFull" });
|
||||
setView("subscribed"); // the sync filter applies to the subscriptions tab
|
||||
setFiltersResetToken((n) => n + 1); // also clear the ephemeral name search + tag chips
|
||||
setPage("channels");
|
||||
}, [setStatusFilter, setView, setPage]);
|
||||
}, [setFilters, setView, setPage]);
|
||||
|
||||
// Clear the manager focus when leaving the Channels page so it doesn't re-apply on a later visit.
|
||||
useEffect(() => {
|
||||
@@ -196,20 +193,20 @@ export function ChannelsProvider({ children }: { children: ReactNode }) {
|
||||
}, []);
|
||||
|
||||
const state = useMemo<ChannelsState>(
|
||||
() => ({ search, statusFilter, view, sort, filtersResetToken, focusName, focusToken }),
|
||||
[search, statusFilter, view, sort, filtersResetToken, focusName, focusToken],
|
||||
() => ({ search, filters, view, sort, filtersResetToken, focusName, focusToken }),
|
||||
[search, filters, view, sort, filtersResetToken, focusName, focusToken],
|
||||
);
|
||||
const actions = useMemo<ChannelsActions>(
|
||||
() => ({
|
||||
setSearch,
|
||||
setStatusFilter,
|
||||
setFilters,
|
||||
setView,
|
||||
setLayout,
|
||||
setSort,
|
||||
focusChannel,
|
||||
goToFullHistory,
|
||||
}),
|
||||
[setSearch, setStatusFilter, setView, setLayout, setSort, focusChannel, goToFullHistory],
|
||||
[setSearch, setFilters, setView, setLayout, setSort, focusChannel, goToFullHistory],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user