feat(playlists): a playlist's menu copies its name and its YouTube address
Every object's action list ends its middle group with Copy name / Copy link (E28 C1, the rows a finger cannot select); the playlist's had neither.
This commit is contained in:
@@ -146,6 +146,7 @@ describe("PlaylistsRail · the row's '…'", () => {
|
||||
await vi.waitFor(() =>
|
||||
expect(screen.getAllByRole("menuitem", { hidden: true }).map((b) => b.textContent)).toEqual([
|
||||
"playlists.rename",
|
||||
"common.copy.name",
|
||||
"playlists.exportToYoutube",
|
||||
"playlists.delete",
|
||||
]),
|
||||
|
||||
@@ -102,7 +102,16 @@ describe("usePlaylistActions — a playlist's menu", () => {
|
||||
|
||||
it("lists a YouTube-linked list with unpushed edits in full", () => {
|
||||
const items = rows({ ...linked, dirty: true });
|
||||
expect(ids(items)).toEqual(["rename", "pin", "open-youtube", "push", "revert", "delete"]);
|
||||
expect(ids(items)).toEqual([
|
||||
"rename",
|
||||
"copy-text",
|
||||
"copy-link",
|
||||
"pin",
|
||||
"open-youtube",
|
||||
"push",
|
||||
"revert",
|
||||
"delete",
|
||||
]);
|
||||
expect(items.find((i) => i.id === "push")!.label).toBe("playlists.pushToYoutube");
|
||||
// One separator opens the YouTube group, one the delete.
|
||||
expect(items.filter((i) => i.separatorBefore).map((i) => i.id)).toEqual([
|
||||
@@ -113,7 +122,7 @@ describe("usePlaylistActions — a playlist's menu", () => {
|
||||
|
||||
it("offers a local list an export and nothing to open or revert", () => {
|
||||
const items = rows({ id: 5, name: "Mine", kind: "user", yt_playlist_id: null, dirty: false });
|
||||
expect(ids(items)).toEqual(["rename", "pin", "push", "delete"]);
|
||||
expect(ids(items)).toEqual(["rename", "copy-text", "pin", "push", "delete"]);
|
||||
expect(items.find((i) => i.id === "push")!.label).toBe("playlists.exportToYoutube");
|
||||
});
|
||||
|
||||
@@ -121,6 +130,8 @@ describe("usePlaylistActions — a playlist's menu", () => {
|
||||
h.canWrite = false;
|
||||
expect(ids(rows({ ...linked, dirty: false }))).toEqual([
|
||||
"rename",
|
||||
"copy-text",
|
||||
"copy-link",
|
||||
"pin",
|
||||
"open-youtube",
|
||||
"delete",
|
||||
@@ -135,7 +146,7 @@ describe("usePlaylistActions — a playlist's menu", () => {
|
||||
yt_playlist_id: null,
|
||||
dirty: false,
|
||||
});
|
||||
expect(ids(items)).toEqual(["rename", "pin", "delete"]);
|
||||
expect(ids(items)).toEqual(["rename", "copy-text", "pin", "delete"]);
|
||||
for (const id of ["rename", "delete"]) {
|
||||
const it = items.find((i) => i.id === id)!;
|
||||
expect(it.disabled).toBe(true);
|
||||
@@ -143,6 +154,14 @@ describe("usePlaylistActions — a playlist's menu", () => {
|
||||
}
|
||||
});
|
||||
|
||||
// E28 review L6: every object's menu copies its name and link (the finger cannot select them);
|
||||
// the playlist's did not.
|
||||
it("copies the list's name, and a linked list's YouTube address", () => {
|
||||
const items = rows(linked);
|
||||
expect(items.find((i) => i.id === "copy-text")!.label).toBe("common.copy.name");
|
||||
expect(items.find((i) => i.id === "copy-link")!.label).toBe("common.copy.link");
|
||||
});
|
||||
|
||||
it("opens the list on YouTube inside the click, in a new tab", () => {
|
||||
const open = vi.spyOn(window, "open").mockReturnValue(null);
|
||||
const row = rows(linked).find((i) => i.id === "open-youtube")!;
|
||||
|
||||
@@ -12,6 +12,7 @@ import { usePlaylists, usePlaylistsActions } from "./playlistsContext";
|
||||
import { useMe } from "../lib/useMe";
|
||||
import { type ActionMenuItem } from "./ui/ActionMenu";
|
||||
import { createStore } from "../lib/store";
|
||||
import { useCopyItems } from "./copyItems";
|
||||
|
||||
// A playlist's ONE action list (E28 C1) and the mutations behind it. Two entry points open it — the
|
||||
// rail row (right-click, long-press, its "…") and the list page header's "…" — so it lives here,
|
||||
@@ -39,6 +40,7 @@ export function usePlaylistActions() {
|
||||
const choose = useChoice();
|
||||
const confirm = useConfirm();
|
||||
const canWrite = useMe().can_write;
|
||||
const copy = useCopyItems();
|
||||
const { selectedId } = usePlaylists();
|
||||
const { setSelectedId } = usePlaylistsActions();
|
||||
const { pushing, reverting } = inFlight.use();
|
||||
@@ -227,6 +229,9 @@ export function usePlaylistActions() {
|
||||
? t("playlists.builtinLocked", { name: t("playlists.watchLater") })
|
||||
: undefined;
|
||||
const busy = (id: number | null) => id === pl.id;
|
||||
const ytUrl = pl.yt_playlist_id
|
||||
? `https://www.youtube.com/playlist?list=${pl.yt_playlist_id}`
|
||||
: null;
|
||||
const youtube: ActionMenuItem[] = [
|
||||
...(pl.yt_playlist_id
|
||||
? [
|
||||
@@ -236,12 +241,7 @@ export function usePlaylistActions() {
|
||||
icon: <ExternalLink className="w-4 h-4" />,
|
||||
// A new tab only opens inside the click (a popup blocker refuses one a task later).
|
||||
inGesture: true,
|
||||
onSelect: () =>
|
||||
void window.open(
|
||||
`https://www.youtube.com/playlist?list=${pl.yt_playlist_id}`,
|
||||
"_blank",
|
||||
"noopener,noreferrer",
|
||||
),
|
||||
onSelect: () => void window.open(ytUrl!, "_blank", "noopener,noreferrer"),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
@@ -284,6 +284,8 @@ export function usePlaylistActions() {
|
||||
note: lockedNote,
|
||||
onSelect: onRename,
|
||||
},
|
||||
// Its name and, when linked, its YouTube address — every object's menu copies them (E28 L6).
|
||||
...copy(playlistName(pl, t), ytUrl, "name"),
|
||||
...pin,
|
||||
...youtube.map((it, i) => ({ ...it, separatorBefore: i === 0 })),
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user