From 0fce732497ecd2bc27edccd5246a97c89f64ea19 Mon Sep 17 00:00:00 2001 From: npeter83 Date: Thu, 8 Oct 2026 21:18:48 +0200 Subject: [PATCH] fix(plex): a playlist row's menu adds its title to a collection too The row menu called the title's action list without the anchor the panel opens beside, and the optional parameter dropped the row silently. The anchor is now required, so every host menu decides. --- .../PlexPlaylistView.menu.dom.test.tsx | 16 ++++++++++++---- frontend/src/components/PlexPlaylistView.tsx | 2 +- frontend/src/components/usePlexCardActions.tsx | 7 ++++--- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/PlexPlaylistView.menu.dom.test.tsx b/frontend/src/components/PlexPlaylistView.menu.dom.test.tsx index c32f9372..caa6a4a1 100644 --- a/frontend/src/components/PlexPlaylistView.menu.dom.test.tsx +++ b/frontend/src/components/PlexPlaylistView.menu.dom.test.tsx @@ -28,6 +28,7 @@ vi.mock("./confirmContext", () => ({ useConfirm: () => h.confirm })); import PlexPlaylistView from "./PlexPlaylistView"; import { api, type PlexCard } from "../lib/api"; +import { qk } from "../lib/queryKeys"; beforeAll(() => { class Stub { @@ -63,15 +64,15 @@ const ep = (id: string, season: number, n: number) => thumb: "", }) as unknown as PlexCard; -function mount(items: PlexCard[], onInfoCard?: (c: PlexCard) => void) { +function mount(items: PlexCard[], onInfoCard?: (c: PlexCard) => void, { admin = false } = {}) { vi.spyOn(api, "plexPlaylist").mockResolvedValue({ id: 5, title: "Films", items } as never); vi.spyOn(api, "comfortPins").mockResolvedValue([] as never); const onPlay = vi.fn(); const onBack = vi.fn(); + const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); + if (admin) client.setQueryData(qk.me(), { role: "admin" }); render( - + , ); @@ -112,6 +113,13 @@ const pressKey = (key: string) => }); describe("PlexPlaylistView · a row's menu", () => { + it("offers an admin a movie's 'Add to collection…', as its card's menu does", async () => { + mount([movie("1")], undefined, { admin: true }); + await screen.findByText("Movie 1"); + await openMenu(() => fireEvent.click(moreOf("Movie 1"))); + expect(menuRows()).toContain("plex.collectionAdd.row"); + }); + it("has no X at rest: each title ends in its '…', which hides until hover or focus", async () => { mount([movie("1"), ep("e1", 1, 1)]); await screen.findByText("Movie 1"); diff --git a/frontend/src/components/PlexPlaylistView.tsx b/frontend/src/components/PlexPlaylistView.tsx index 5b78febd..339bb627 100644 --- a/frontend/src/components/PlexPlaylistView.tsx +++ b/frontend/src/components/PlexPlaylistView.tsx @@ -626,7 +626,7 @@ export default function PlexPlaylistView({ ? [] : menuTarget.kind === "item" ? [ - ...titleActions.items(menuTarget.item), + ...titleActions.items(menuTarget.item, { anchor: rowMenu.anchor }), ...orderRows(menuTarget), removeRow(menuTarget, t("plex.playlist.removeFromPlaylist")), ] diff --git a/frontend/src/components/usePlexCardActions.tsx b/frontend/src/components/usePlexCardActions.tsx index 846f57ac..2b391e2f 100644 --- a/frontend/src/components/usePlexCardActions.tsx +++ b/frontend/src/components/usePlexCardActions.tsx @@ -61,14 +61,15 @@ export function usePlexCardActions(handlers: PlexActionHandlers, open: boolean) /** `play: false` where the host menu leads with its own Play row (Home's shelf resolves `next`); * `pin: false` where the host carries its own shelf rows or the object is not a pin kind; * `anchor`: the element the menu came from, which "Add to collection…" opens its panel beside — - * without one the row is left out rather than opening the panel at a corner. */ + * without one the row is left out rather than opening the panel at a corner. Required, so every + * host menu decides: an optional one let a menu that has an anchor drop the row silently. */ const items = ( c: PlexCard, { play = true, pin: withPin = true, - anchor = null, - }: { play?: boolean; pin?: boolean; anchor?: Element | null } = {}, + anchor, + }: { play?: boolean; pin?: boolean; anchor: Element | null }, ): ActionMenuItem[] => { const { onOpen, onInfo, onToggleWatched, onRestart } = handlers; const playable = isPlexPlayable(c);