fix(notifications): a notice value is no plain string, so a translated name cannot freeze in one language
A name goes in as raw(), a translated word as its own msg(): tsc now lists every value, and the ones that froze are said when shown — Watch later and an untitled channel in the pin and channel notices, a Plex season's name in its removal toast (a new seasonTitleText, which seasonTitle says).
This commit is contained in:
@@ -29,7 +29,7 @@ vi.mock("../../components/ui/confirmContext", () => ({ useConfirm: () => h.confi
|
||||
import PlexPlaylistView from "./PlexPlaylistView";
|
||||
import { api, type PlexCard } from "../../lib/api";
|
||||
import { qk } from "../../lib/queryKeys";
|
||||
import { msg, type NotifText } from "../../lib/notifications";
|
||||
import { msg, type NotifText, raw } from "../../lib/notifications";
|
||||
|
||||
beforeAll(() => {
|
||||
class Stub {
|
||||
@@ -183,7 +183,9 @@ describe("PlexPlaylistView · a row's menu", () => {
|
||||
await waitFor(() => expect(api.plexPlaylistRemoveItem).toHaveBeenCalledWith(5, "2"));
|
||||
expect(h.confirm).not.toHaveBeenCalled();
|
||||
await waitFor(() =>
|
||||
expect(toast()?.message).toEqual(msg("plex.playlist.removedToast", { title: "Movie 2" })),
|
||||
expect(toast()?.message).toEqual(
|
||||
msg("plex.playlist.removedToast", { title: raw("Movie 2") }),
|
||||
),
|
||||
);
|
||||
act(() => toast()!.action!.onClick());
|
||||
await waitFor(() => expect(add).toHaveBeenCalledWith(5, ["2"], [1]));
|
||||
@@ -212,7 +214,9 @@ describe("PlexPlaylistView · a row's menu", () => {
|
||||
await pick(() => fireEvent.click(moreOf("The Show")), "plex.playlist.removeShow");
|
||||
await waitFor(() => expect(bulk).toHaveBeenCalledWith(5, ["e1", "e2"]));
|
||||
await waitFor(() =>
|
||||
expect(toast()?.message).toEqual(msg("plex.playlist.removedToast", { title: "The Show" })),
|
||||
expect(toast()?.message).toEqual(
|
||||
msg("plex.playlist.removedToast", { title: raw("The Show") }),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -232,6 +236,17 @@ describe("PlexPlaylistView · a row's menu", () => {
|
||||
.find((b) => b.textContent === "plex.playlist.removeSeason")!,
|
||||
);
|
||||
await waitFor(() => expect(bulk).toHaveBeenCalledWith(5, ["e2", "e3"]));
|
||||
// The season's name is kept as what to say, so the toast follows a language switch.
|
||||
await waitFor(() =>
|
||||
expect(toast()?.message).toEqual(
|
||||
msg("plex.playlist.removedToast", {
|
||||
title: msg("plex.playlist.seasonOfShow", {
|
||||
show: raw("The Show"),
|
||||
season: msg("plex.seasonTitle", { n: 2 }),
|
||||
}),
|
||||
}),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -55,10 +55,10 @@ import { useReturnFocus } from "../../components/ui/useReturnFocus";
|
||||
import { SURFACE } from "../../components/ui/surfaces";
|
||||
import { PageHeading } from "../../components/ui/Headings";
|
||||
import { MEDIA_BADGE } from "../../components/ui/mediaCardStyles";
|
||||
import { seasonTitle } from "../../lib/plexDetail";
|
||||
import { seasonTitle, seasonTitleText } from "../../lib/plexDetail";
|
||||
import { cardImage } from "../../lib/plexImage";
|
||||
import RemoteImage from "../../components/ui/RemoteImage";
|
||||
import { msg } from "../../lib/notifications";
|
||||
import { msg, raw, type NotifText } from "../../lib/notifications";
|
||||
|
||||
// A playlist's items, grouped for readability: movies stay standalone, while a run of episodes from the
|
||||
// same show collapses into a season/show group so a whole series doesn't sprawl into an endless flat
|
||||
@@ -563,7 +563,6 @@ export default function PlexPlaylistView({
|
||||
// Keyed: a row builds its target afresh each render, and its "…" must close the menu it opened.
|
||||
const rowMenu = useContextMenu<MenuTarget>({ keyOf: targetKey });
|
||||
const copy = useCopyItems();
|
||||
const seasonLabel = useSeasonLabel();
|
||||
const titleActions = usePlexCardActions(
|
||||
{
|
||||
onOpen: (c) => onPlay(c.id, order),
|
||||
@@ -581,14 +580,17 @@ export default function PlexPlaylistView({
|
||||
rowMenu.open,
|
||||
);
|
||||
// What a row's removal takes out, and the name its toast says.
|
||||
const removal = (target: MenuTarget): { keys: string[]; title: string } =>
|
||||
const removal = (target: MenuTarget): { keys: string[]; title: NotifText } =>
|
||||
target.kind === "item"
|
||||
? { keys: [target.item.id], title: target.item.title }
|
||||
? { keys: [target.item.id], title: raw(target.item.title) }
|
||||
: target.kind === "show"
|
||||
? { keys: target.block.items.map((i) => i.id), title: target.block.title }
|
||||
? { keys: target.block.items.map((i) => i.id), title: raw(target.block.title) }
|
||||
: {
|
||||
keys: target.season.items.map((i) => i.id),
|
||||
title: `${target.block.title} · ${seasonLabel(target.season)}`,
|
||||
title: msg("plex.playlist.seasonOfShow", {
|
||||
show: raw(target.block.title),
|
||||
season: seasonTitleText({ season_number: target.season.number }),
|
||||
}),
|
||||
};
|
||||
const remove = (target: MenuTarget) => {
|
||||
const { keys, title } = removal(target);
|
||||
|
||||
@@ -27,7 +27,7 @@ vi.mock("../../lib/notifications", async (importOriginal) => ({
|
||||
import { usePlexPlaylistEdits } from "./usePlexPlaylistEdits";
|
||||
import { api, type PlexCard } from "../../lib/api";
|
||||
import { qk } from "../../lib/queryKeys";
|
||||
import { msg, type NotifText } from "../../lib/notifications";
|
||||
import { msg, type NotifText, raw } from "../../lib/notifications";
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
@@ -125,7 +125,7 @@ describe("usePlexPlaylistEdits · the removal", () => {
|
||||
await waitFor(() => expect(hook.result.current.items).toHaveLength(2));
|
||||
vi.spyOn(api, "plexPlaylistRemoveItem").mockRejectedValue(new Error("offline"));
|
||||
|
||||
await act(() => hook.result.current.onRemove(["2"], "Movie 2"));
|
||||
await act(() => hook.result.current.onRemove(["2"], raw("Movie 2")));
|
||||
expect(errors()).toEqual([msg("plex.playlist.removeFailed")]);
|
||||
expect(ids(hook.result.current.items)).toEqual(["1", "2"]);
|
||||
expect(hook.result.current.busy).toBe(false);
|
||||
@@ -144,9 +144,11 @@ describe("usePlexPlaylistEdits · the removal", () => {
|
||||
const add = vi.spyOn(api, "plexPlaylistAddBulk").mockResolvedValue({} as never);
|
||||
|
||||
// A season whose episodes do not sit together: rows 2 and 4.
|
||||
await act(() => hook.result.current.onRemove(["4", "2"], "Show · Season 1"));
|
||||
await act(() => hook.result.current.onRemove(["4", "2"], raw("Show · Season 1")));
|
||||
const toast = notify.mock.calls.map(([n]) => n).find((n) => n.action)!;
|
||||
expect(toast.message).toEqual(msg("plex.playlist.removedToast", { title: "Show · Season 1" }));
|
||||
expect(toast.message).toEqual(
|
||||
msg("plex.playlist.removedToast", { title: raw("Show · Season 1") }),
|
||||
);
|
||||
expect(toast.action!.label).toBe("common.undo");
|
||||
act(() => toast.action!.onClick());
|
||||
await waitFor(() => expect(add).toHaveBeenCalledTimes(1));
|
||||
@@ -160,7 +162,7 @@ describe("usePlexPlaylistEdits · the removal", () => {
|
||||
await waitFor(() => expect(hook.result.current.items).toHaveLength(3));
|
||||
vi.spyOn(api, "plexPlaylistRemoveItem").mockResolvedValue({} as never);
|
||||
const add = vi.spyOn(api, "plexPlaylistAddBulk").mockResolvedValue({} as never);
|
||||
await act(() => hook.result.current.onRemove(["2"], "Movie 2"));
|
||||
await act(() => hook.result.current.onRemove(["2"], raw("Movie 2")));
|
||||
detail.mockResolvedValue({
|
||||
id: 5,
|
||||
title: "F",
|
||||
@@ -179,7 +181,7 @@ describe("usePlexPlaylistEdits · the removal", () => {
|
||||
return {} as never;
|
||||
});
|
||||
vi.spyOn(api, "plexPlaylistAddBulk").mockRejectedValue(new Error("offline"));
|
||||
await act(() => hook.result.current.onRemove(["1"], "Movie 1"));
|
||||
await act(() => hook.result.current.onRemove(["1"], raw("Movie 1")));
|
||||
const toast = notify.mock.calls.map(([n]) => n).find((n) => n.action)!;
|
||||
act(() => toast.action!.onClick());
|
||||
await waitFor(() => expect(errors()).toEqual([msg("plex.playlist.undoRemoveFailed")]));
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { plexLang } from "../../i18n";
|
||||
import { api, type PlexCard } from "../../lib/api";
|
||||
import { notifyActionError } from "../../lib/actionErrors";
|
||||
import { msg, notify } from "../../lib/notifications";
|
||||
import { msg, type NotifText, notify } from "../../lib/notifications";
|
||||
import { qk } from "../../lib/queryKeys";
|
||||
import { reinsertAt } from "../../lib/reinsert";
|
||||
|
||||
@@ -88,7 +88,7 @@ export function usePlexPlaylistEdits(playlistId: number) {
|
||||
* each row back where it stood (C1 in DESIGN.md: the same bargain as a YouTube playlist's rows) — after
|
||||
* its old neighbour in the list as it is at Undo time (`reinsertAt`), since a raw index drifts
|
||||
* once anything changes in between. `title` names what went in that toast. */
|
||||
async function onRemove(keys: string[], title: string) {
|
||||
async function onRemove(keys: string[], title: NotifText) {
|
||||
if (busy || !keys.length) return;
|
||||
setBusy(true);
|
||||
// The list as it stands now, which is the server's order: a season's episodes need not sit
|
||||
|
||||
Reference in New Issue
Block a user