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);