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:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user