feat(channels): shareable manager view in the URL — tab+sort+column-filters (S8)
The Channels-manager content state now round-trips through the address bar,
so a manager view is bookmarkable/shareable — the S4 pattern applied to the
last content page the audit flagged.
- New lib/channelsUrlState.ts (channelsParamUpdates / channelsParamsToState /
hasChannelsParams) under NAMESPACED keys ctab/csort/cf — channels-unique
like ?v=/?pl/pscope, so no other provider reads or clears them and the
feed's URL-stripping can't delete them. The mixed scalar/array FilterMap
(sync/priority scalars + tags multi) rides in `cf` as a JSON blob so both
shapes round-trip exactly. Only non-default values emit.
- ChannelsProvider mirrors {view, sort, filters} via useUrlMirror, page-gated
on channels (off-page the keys clear; localStorage still holds the state and
it re-mirrors on return). A shared link wins over stored state: readChannels
UrlSeed captured once, applied before paint through the slice setters (so the
account-pin re-hydrate sees them dirty and won't reload storage over the
shared view), then persisted to the account once pinned so it survives a
later plain reload — mirrors PlexProvider's seed/rehydrate. The name search
stays ephemeral (out of the shared contract).
- NavigationProvider's page-switch nulls ctab/csort/cf alongside the plex keys
so switching modules resets to the manager root.
Tests: channelsUrlState.dom.test.ts (9 round-trip) + ChannelsProvider.dom
seed/mirror (2); the existing tab-history test's beforeEach now resets the
query string too (the mirror writes it). FE vitest 503→514. FE-only.
This commit is contained in:
@@ -27,7 +27,10 @@ const VIEW_KEY = "siftlode.channelsView.1"; // account-scoped (accountKey suffix
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
sessionStorage.setItem("siftlode.activeAccount", "1"); // pin the tab's account
|
||||
window.history.replaceState({ sfPage: "channels" }, ""); // setView only owns an entry on this page
|
||||
// Reset BOTH the history state and the query string: setView only owns an entry on the channels page,
|
||||
// and the S8 URL mirror writes ctab/csort/cf — a stale key left by a prior test would be read back by
|
||||
// this provider's mount-time URL seed and change the starting tab.
|
||||
window.history.replaceState({ sfPage: "channels" }, "", "/");
|
||||
});
|
||||
|
||||
describe("ChannelsProvider tab history", () => {
|
||||
@@ -71,3 +74,29 @@ describe("ChannelsProvider tab history", () => {
|
||||
expect(result.current.view).toBe("hidden"); // unchanged
|
||||
});
|
||||
});
|
||||
|
||||
describe("ChannelsProvider — S8 shareable URL", () => {
|
||||
it("seeds tab + sort + filters from a shared manager link at mount (URL wins over stored)", () => {
|
||||
// Stored view says "blocked"; the shared link says "hidden" + a sort + a filter → the URL wins.
|
||||
localStorage.setItem(VIEW_KEY, "blocked");
|
||||
const cf = encodeURIComponent(JSON.stringify({ sync: "needsFull" }));
|
||||
window.history.replaceState(
|
||||
{ sfPage: "channels" },
|
||||
"",
|
||||
`/?ctab=hidden&csort=priority:desc&cf=${cf}`,
|
||||
);
|
||||
const { result } = render();
|
||||
expect(result.current.view).toBe("hidden");
|
||||
expect(result.current.sort).toEqual({ key: "priority", dir: "desc" });
|
||||
expect(result.current.filters).toEqual({ sync: "needsFull" });
|
||||
});
|
||||
|
||||
it("mirrors the tab into the address bar and clears it back to the default", () => {
|
||||
const { result } = render();
|
||||
act(() => result.current.setView("blocked"));
|
||||
expect(new URLSearchParams(window.location.search).get("ctab")).toBe("blocked");
|
||||
// Back to the default tab clears the key (only non-defaults are emitted).
|
||||
act(() => result.current.setView("subscribed"));
|
||||
expect(new URLSearchParams(window.location.search).has("ctab")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user