fix(notifications): a notice's button is said when shown, like its text
An action label was translated when the notice was raised, so after a language switch a live toast or center row read in one language with its button in the other. The access-requests nudge no longer re-issues on a switch for it. New tests: the button and a server refusal follow a switch.
This commit is contained in:
@@ -30,13 +30,12 @@ import { quickShareLink } from "./downloads";
|
||||
import { qk } from "./queryKeys";
|
||||
import { msg, type NotifText } from "./notifications";
|
||||
|
||||
const t = (k: string) => k;
|
||||
const origin = window.location.origin;
|
||||
|
||||
type ToastArg = {
|
||||
level: string;
|
||||
message: NotifText;
|
||||
action?: { label: string; onClick: () => void };
|
||||
action?: { label: NotifText; onClick: () => void };
|
||||
};
|
||||
const firstToast = () => notify.mock.calls[0]![0] as ToastArg;
|
||||
|
||||
@@ -59,7 +58,7 @@ describe("quickShareLink", () => {
|
||||
createDownloadLink.mockResolvedValue({ id: 7, url: "/watch/tok7" });
|
||||
const openSpy = vi.spyOn(window, "open").mockReturnValue(null);
|
||||
|
||||
const url = await quickShareLink(qc(), { kind: "file", id: 42 }, t);
|
||||
const url = await quickShareLink(qc(), { kind: "file", id: 42 });
|
||||
|
||||
expect(url).toBe(`${origin}/watch/tok7`);
|
||||
// Minted with an EMPTY body so the backend default (allow_download=true) applies.
|
||||
@@ -68,7 +67,7 @@ describe("quickShareLink", () => {
|
||||
const arg = firstToast();
|
||||
expect(arg.level).toBe("success");
|
||||
expect(arg.message).toEqual(msg("downloads.share.linkCopied"));
|
||||
expect(arg.action!.label).toBe("downloads.share.openNewTab");
|
||||
expect(arg.action!.label).toEqual(msg("downloads.share.openNewTab"));
|
||||
arg.action!.onClick();
|
||||
expect(openSpy).toHaveBeenCalledWith(`${origin}/watch/tok7`, "_blank", "noopener,noreferrer");
|
||||
});
|
||||
@@ -76,7 +75,7 @@ describe("quickShareLink", () => {
|
||||
it("reuses an existing link instead of minting a duplicate", async () => {
|
||||
downloadLinks.mockResolvedValue([{ id: 3, url: "/watch/old" }]);
|
||||
|
||||
const url = await quickShareLink(qc(), { kind: "file", id: 1 }, t);
|
||||
const url = await quickShareLink(qc(), { kind: "file", id: 1 });
|
||||
|
||||
expect(url).toBe(`${origin}/watch/old`);
|
||||
expect(createDownloadLink).not.toHaveBeenCalled();
|
||||
@@ -89,7 +88,7 @@ describe("quickShareLink", () => {
|
||||
{ id: 3, url: "/watch/older" },
|
||||
]);
|
||||
|
||||
const url = await quickShareLink(qc(), { kind: "file", id: 1 }, t);
|
||||
const url = await quickShareLink(qc(), { kind: "file", id: 1 });
|
||||
|
||||
expect(url).toBe(`${origin}/watch/older`);
|
||||
expect(createDownloadLink).not.toHaveBeenCalled();
|
||||
@@ -101,7 +100,7 @@ describe("quickShareLink", () => {
|
||||
createDownloadLink.mockResolvedValue({ id: 8, url: "/watch/new" });
|
||||
const client = qc();
|
||||
|
||||
const url = await quickShareLink(client, { kind: "file", id: 6 }, t);
|
||||
const url = await quickShareLink(client, { kind: "file", id: 6 });
|
||||
|
||||
expect(url).toBe(`${origin}/watch/new`);
|
||||
expect(copyText).toHaveBeenCalledWith(`${origin}/watch/new`);
|
||||
@@ -117,7 +116,7 @@ describe("quickShareLink", () => {
|
||||
const client = qc();
|
||||
client.setQueryData(qk.fileFolders(), []);
|
||||
|
||||
const url = await quickShareLink(client, { kind: "folder", id: 5 }, t);
|
||||
const url = await quickShareLink(client, { kind: "folder", id: 5 });
|
||||
|
||||
expect(url).toBe(`${origin}/watch/fold`);
|
||||
expect(folderLinks).toHaveBeenCalledWith(5);
|
||||
@@ -133,20 +132,20 @@ describe("quickShareLink", () => {
|
||||
downloadLinks.mockResolvedValue([{ id: 5, url: "/watch/x" }]);
|
||||
copyText.mockResolvedValue(false);
|
||||
|
||||
const url = await quickShareLink(qc(), { kind: "file", id: 2 }, t);
|
||||
const url = await quickShareLink(qc(), { kind: "file", id: 2 });
|
||||
|
||||
expect(url).toBe(`${origin}/watch/x`); // the link is never a dead end
|
||||
const arg = firstToast();
|
||||
expect(arg.level).toBe("warning");
|
||||
expect(arg.message).toEqual(msg("downloads.share.copyFailed"));
|
||||
expect(arg.action!.label).toBe("downloads.share.openNewTab");
|
||||
expect(arg.action!.label).toEqual(msg("downloads.share.openNewTab"));
|
||||
});
|
||||
|
||||
it("reports an error and returns null when link creation fails", async () => {
|
||||
downloadLinks.mockResolvedValue([]);
|
||||
createDownloadLink.mockRejectedValue(new Error("boom"));
|
||||
|
||||
const url = await quickShareLink(qc(), { kind: "file", id: 9 }, t);
|
||||
const url = await quickShareLink(qc(), { kind: "file", id: 9 });
|
||||
|
||||
expect(url).toBeNull();
|
||||
expect(copyText).not.toHaveBeenCalled();
|
||||
|
||||
@@ -44,11 +44,7 @@ export function invalidateLinks(qc: QueryClient, target: LinkTarget) {
|
||||
// new tab" action. A copy failure still toasts (warning) WITH that action, so the link is never a dead
|
||||
// end. Advanced controls (password, expiry, revoke, share-with-a-user) live in the Share dialog.
|
||||
// Returns the shared URL, or null if link creation failed — so the caller/tests can react to the outcome.
|
||||
export async function quickShareLink(
|
||||
qc: QueryClient,
|
||||
target: LinkTarget,
|
||||
t: (key: string) => string,
|
||||
): Promise<string | null> {
|
||||
export async function quickShareLink(qc: QueryClient, target: LinkTarget): Promise<string | null> {
|
||||
let path;
|
||||
try {
|
||||
const existing = await qc.fetchQuery({
|
||||
@@ -70,7 +66,7 @@ export async function quickShareLink(
|
||||
}
|
||||
const url = window.location.origin + path;
|
||||
const action = {
|
||||
label: t("downloads.share.openNewTab"),
|
||||
label: msg("downloads.share.openNewTab"),
|
||||
onClick: () => window.open(url, "_blank", "noopener,noreferrer"),
|
||||
};
|
||||
notify(
|
||||
|
||||
@@ -20,7 +20,7 @@ afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
const undo = { label: "Undo", onClick: () => {} };
|
||||
const undo = { label: raw("Undo"), onClick: () => {} };
|
||||
const shown = (id: number) => getActiveToasts().some((n) => n.id === id);
|
||||
|
||||
describe("an action toast's lifetime", () => {
|
||||
|
||||
@@ -66,7 +66,8 @@ const LOG_LEVEL: Record<NotifLevel, Level> = {
|
||||
export type NotifLevel = "info" | "success" | "warning" | "error" | "fatal";
|
||||
|
||||
interface NotifAction {
|
||||
label: string;
|
||||
// Said when shown, like the notice's text: a language switch must not leave the button behind.
|
||||
label: NotifText;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import { HttpError } from "./api";
|
||||
import { markReported } from "./api/core";
|
||||
import { msg, notify, type NotifText } from "./notifications";
|
||||
import { notifyActionError } from "./actionErrors";
|
||||
import i18n from "../i18n";
|
||||
|
||||
// A YouTube-gated action (subscribe, sync, backfill, unsubscribe) that 403s means the user hasn't
|
||||
// granted the write scope — surface a "Connect" affordance instead of a vague failure. Pass
|
||||
@@ -19,9 +18,7 @@ export function notifyYouTubeActionError(
|
||||
notify({
|
||||
level: "error",
|
||||
message: msg("channels.notify.needYouTube"),
|
||||
action: onConnect
|
||||
? { label: i18n.t("channels.notify.connect"), onClick: onConnect }
|
||||
: undefined,
|
||||
action: onConnect ? { label: msg("channels.notify.connect"), onClick: onConnect } : undefined,
|
||||
});
|
||||
markReported(err);
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user