refactor(frontend): the module stores mount from the registry, and the feed and Plex each own their saved views
A module's definition names its store (feed: filters, view, saved views; Plex: browse state, saved views; channels, files, playlists their provider), and ModuleStores mounts them at the root in registry order instead of a hand-nested tree in main.tsx. A contract test holds that every store's read side is in reach and that no store's static imports leave its own module, so the order cannot matter. Saved views were one provider that switched between the feed and Plex by the active page and reset on each switch. The logic is now one hook (useSavedViewsStore) with an instance per module; the shared chip, popover and modal read the views their page or rail hands down. A feed view applied and edited now survives a visit to Plex. Plex's views are still asked for only on its page, and a module's manage modal still closes when its page is left. The feed's URL-override check applies only on the feed page, where its filter params live. The Channels store's tab type moves from the page into the store's context.
This commit is contained in:
@@ -43,12 +43,20 @@ vi.mock("../lib/keymap", () => ({
|
||||
toggleKeyboardHelp: vi.fn(),
|
||||
}));
|
||||
|
||||
// Each module's saved views, and the page the palette opens over.
|
||||
const views = {
|
||||
feed: [] as { id: number; name: string }[],
|
||||
plex: [] as { id: number; name: string }[],
|
||||
};
|
||||
const nav = { page: "feed" };
|
||||
|
||||
const calls: string[] = [];
|
||||
const setPage = vi.fn((page: string, then?: () => void) => {
|
||||
calls.push(`setPage:${page}`);
|
||||
then?.();
|
||||
});
|
||||
vi.mock("../features/navigation/navigationContext", () => ({
|
||||
useNavigation: () => ({ page: nav.page }),
|
||||
useNavigationActions: () => ({ setPage, openChannel: vi.fn() }),
|
||||
}));
|
||||
vi.mock("../modules/playlists/playlistsContext", () => ({
|
||||
@@ -59,8 +67,13 @@ vi.mock("../modules/playlists/playlistsContext", () => ({
|
||||
vi.mock("../features/prefs/prefsContext", () => ({
|
||||
usePrefs: () => ({ theme: { scheme: "default" }, setTheme: vi.fn() }),
|
||||
}));
|
||||
vi.mock("../features/filters/savedViewsContext", () => ({
|
||||
useSavedViews: () => ({ views: [], apply: vi.fn() }),
|
||||
vi.mock("../modules/feed/feedSavedViews", async (importOriginal) => ({
|
||||
...(await importOriginal<Record<string, unknown>>()),
|
||||
useFeedSavedViews: () => ({ views: views.feed, apply: vi.fn() }),
|
||||
}));
|
||||
vi.mock("../modules/plex/plexSavedViews", async (importOriginal) => ({
|
||||
...(await importOriginal<Record<string, unknown>>()),
|
||||
usePlexSavedViews: () => ({ views: views.plex, apply: vi.fn() }),
|
||||
}));
|
||||
const requestSurprise = vi.fn();
|
||||
vi.mock("../lib/homeIntent", () => ({ requestSurprise: () => requestSurprise() }));
|
||||
@@ -96,6 +109,9 @@ beforeEach(() => {
|
||||
requestSurprise.mockClear();
|
||||
channels.mockReset().mockResolvedValue([]);
|
||||
playlists.mockReset().mockResolvedValue([]);
|
||||
views.feed = [];
|
||||
views.plex = [];
|
||||
nav.page = "feed";
|
||||
});
|
||||
afterEach(cleanup);
|
||||
|
||||
@@ -114,6 +130,18 @@ describe("AppCommandPalette", () => {
|
||||
expect(ids("modules")).toContain("page:config");
|
||||
});
|
||||
|
||||
it("offers Plex's saved views over the Plex page, and the feed's over any other", () => {
|
||||
views.feed = [{ id: 1, name: "feed view" }];
|
||||
views.plex = [{ id: 2, name: "plex view" }];
|
||||
nav.page = "plex";
|
||||
mount();
|
||||
expect(ids("views")).toEqual(["view:2"]);
|
||||
cleanup();
|
||||
nav.page = "home";
|
||||
mount();
|
||||
expect(ids("views")).toEqual(["view:1"]);
|
||||
});
|
||||
|
||||
it("leaves out the channels the user has hidden", async () => {
|
||||
channels.mockResolvedValue([
|
||||
{ id: "UC1", title: "Shown", handle: "@shown", hidden: false },
|
||||
|
||||
@@ -23,10 +23,11 @@ import { requestSurprise } from "../lib/homeIntent";
|
||||
import { requestLinkBox } from "../lib/downloadIntake";
|
||||
import { openRecorderStart } from "../lib/debugRecorder";
|
||||
import { useDebugRecording } from "../features/diagnostics/useDebugRecording";
|
||||
import { useNavigationActions } from "../features/navigation/navigationContext";
|
||||
import { useNavigation, useNavigationActions } from "../features/navigation/navigationContext";
|
||||
import { useFeedSavedViews } from "../modules/feed";
|
||||
import { usePlaylistsActions } from "../modules/playlists";
|
||||
import { usePlexSavedViews } from "../modules/plex";
|
||||
import { usePrefs } from "../features/prefs/prefsContext";
|
||||
import { useSavedViews } from "../features/filters/savedViewsContext";
|
||||
|
||||
// What Ctrl/Cmd+K can reach. The generic list/filter/keyboard shell is `ui/CommandPalette`; this is
|
||||
// the domain half — the layer boundary says a primitive may not import `lib/api` or a module.
|
||||
@@ -37,8 +38,8 @@ import { useSavedViews } from "../features/filters/savedViewsContext";
|
||||
// the Channels page already gives it, and one mistyped Enter is a worse failure than the walk it
|
||||
// would have saved.
|
||||
//
|
||||
// Saved views come from whichever module owns the current SavedViewsProvider source (feed on the
|
||||
// feed, Plex on Plex), i.e. exactly the views that are applicable where you stand.
|
||||
// Saved views are Plex's on the Plex page and the feed's everywhere else, i.e. the views that are
|
||||
// applicable where you stand.
|
||||
export default function AppCommandPalette() {
|
||||
const { t } = useTranslation();
|
||||
const open = usePaletteOpen();
|
||||
@@ -46,7 +47,10 @@ export default function AppCommandPalette() {
|
||||
const { setPage, openChannel } = useNavigationActions();
|
||||
const { setSelectedId } = usePlaylistsActions();
|
||||
const { theme, setTheme } = usePrefs();
|
||||
const savedViews = useSavedViews();
|
||||
const { page } = useNavigation();
|
||||
const feedViews = useFeedSavedViews();
|
||||
const plexViews = usePlexSavedViews();
|
||||
const savedViews = page === "plex" ? plexViews : feedViews;
|
||||
const { session: recordingSession, stop: stopRecording, markStep } = useDebugRecording();
|
||||
const stopMutate = stopRecording.mutate;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user