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:
2026-08-14 02:00:17 +02:00
parent 665bc6f7b5
commit 837fdcd408
6 changed files with 181 additions and 61 deletions
+28 -4
View File
@@ -53,6 +53,8 @@ export default function DataTable<T>({
resetFiltersToken,
sort: sortProp,
onSortChange,
filters: filtersProp,
onFiltersChange,
}: {
rows: T[];
columns: Column<T>[];
@@ -82,6 +84,13 @@ export default function DataTable<T>({
// persistKey-backed sort.
sort?: SortState;
onSortChange?: (next: SortState) => void;
// CONTROLLED filters (optional): symmetric to controlled sort. When `onFiltersChange` is given the
// caller owns the column-filter map (and its persistence) — so the channel manager's top status
// chips, the Sync column funnel, and the card layout all share ONE filter state — and the table
// reflects `filters` and reports funnel changes instead of keeping its own. Omit both for the
// default internal, persistKey-backed filters.
filters?: FilterMap;
onFiltersChange?: (next: FilterMap) => void;
}) {
const { t } = useTranslation();
const initial = loadPersist(persistKey);
@@ -90,17 +99,31 @@ export default function DataTable<T>({
const controlledSort = onSortChange !== undefined;
const [internalSort, setInternalSort] = useState<SortState>(initial.sort);
const sort = controlledSort ? (sortProp ?? null) : internalSort;
const [filters, setFilters] = useState<FilterMap>(initial.filters);
// Filters are controlled when the caller passes onFiltersChange (it then owns/persists the map);
// otherwise the table keeps its own, seeded from and saved to persistKey — symmetric to sort.
const controlledFilters = onFiltersChange !== undefined;
const [internalFilters, setInternalFilters] = useState<FilterMap>(initial.filters);
const filters = controlledFilters ? (filtersProp ?? {}) : internalFilters;
const [page, setPage] = useState(initial.page);
const [size, setSize] = useState(initial.size ?? pageSize);
// Commit a whole new filter map through the owner (controlled) or internal state. A ref holds the
// latest resolved map so the externalFilter/reset effects can derive from it without taking
// `filters` as a dep (which would re-run them on every filter change).
const filtersRef = useRef(filters);
filtersRef.current = filters;
const commitFilters = (next: FilterMap) => {
if (controlledFilters) onFiltersChange!(next);
else setInternalFilters(next);
};
useEffect(() => {
if (persistKey) localStorage.setItem(persistKey, JSON.stringify({ sort, filters, page, size }));
}, [persistKey, sort, filters, page, size]);
useEffect(() => {
if (!externalFilter) return;
setFilters((f) => ({ ...f, [externalFilter.key]: externalFilter.value }));
commitFilters({ ...filtersRef.current, [externalFilter.key]: externalFilter.value });
setPage(0);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [externalFilter?.key, externalFilter?.value]);
@@ -113,8 +136,9 @@ export default function DataTable<T>({
useEffect(() => {
if (resetFiltersToken === undefined || resetFiltersToken === resetRef.current) return;
resetRef.current = resetFiltersToken;
setFilters({});
commitFilters({});
setPage(0);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [resetFiltersToken]);
const sorted = sortRows(filterRows(rows, columns, filters), columns, sort);
@@ -132,7 +156,7 @@ export default function DataTable<T>({
setPage(0);
}
function applyFilter(key: string, value: string | string[]) {
setFilters((f) => ({ ...f, [key]: value }));
commitFilters({ ...filters, [key]: value });
setPage(0);
}