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:
@@ -26,6 +26,9 @@ export const LS = {
|
||||
// chips, the Sync column funnel, and the card layout all share ONE state (per-account). Supersedes
|
||||
// the old single-string `channelFilter` sync chip.
|
||||
channelsFilterMap: "siftlode.channelsFilterMap",
|
||||
// DEPRECATED — the retired single-value sync chip; read once to migrate a pinned value into
|
||||
// `channelsFilterMap`, then inert. Do not write.
|
||||
legacyChannelFilter: "siftlode.channelFilter",
|
||||
channelsView: "siftlode.channelsView",
|
||||
// The channel manager's layout (table / cards — see lib/channelLayout.ts), per-account.
|
||||
channelLayout: "siftlode.channelLayout",
|
||||
|
||||
@@ -53,4 +53,19 @@ describe("useFilterState", () => {
|
||||
const { result } = renderHook(() => useFilterState("k"));
|
||||
expect(result.current[0]).toEqual({ sync: "needsFull" });
|
||||
});
|
||||
|
||||
it("uses the migrate seed only when storage is empty", () => {
|
||||
const migrate = () => ({ sync: "needsFull" });
|
||||
// Empty storage → seed applies.
|
||||
expect(renderHook(() => useFilterState("k", migrate)).result.current[0]).toEqual({
|
||||
sync: "needsFull",
|
||||
});
|
||||
// Stored value wins over the seed.
|
||||
store.set("k2", JSON.stringify({ sync: "fully" }));
|
||||
expect(renderHook(() => useFilterState("k2", migrate)).result.current[0]).toEqual({
|
||||
sync: "fully",
|
||||
});
|
||||
// A seed that returns null falls back to an empty map.
|
||||
expect(renderHook(() => useFilterState("k3", () => null)).result.current[0]).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,8 +6,18 @@ import type { FilterMap } from "../components/tableHeader";
|
||||
// caller (so a second view of the same rows — the channel manager's card grid, the top status chips
|
||||
// — shares one filter state). The table analogue of useSortState: one home so a controlled-filter
|
||||
// surface gets persistence for free, seeded from and saved to `storageBase`.
|
||||
export function useFilterState(storageBase: string): [FilterMap, (next: FilterMap) => void] {
|
||||
const [filters, setFilters] = useState<FilterMap>(() => hydrate(getAccountRaw(storageBase)));
|
||||
export function useFilterState(
|
||||
storageBase: string,
|
||||
// Optional one-time seed used ONLY when `storageBase` has nothing stored yet — e.g. to carry a
|
||||
// retired single-value key forward into the new map. Runs synchronously at init (no first-render
|
||||
// flash); it stays idempotent because the seed persists as soon as the user touches the filters.
|
||||
migrate?: () => FilterMap | null,
|
||||
): [FilterMap, (next: FilterMap) => void] {
|
||||
const [filters, setFilters] = useState<FilterMap>(() => {
|
||||
const raw = getAccountRaw(storageBase);
|
||||
if (raw) return hydrate(raw);
|
||||
return migrate?.() ?? {};
|
||||
});
|
||||
const set = useCallback(
|
||||
(next: FilterMap) => {
|
||||
setFilters(next);
|
||||
|
||||
Reference in New Issue
Block a user