fix(inbox): a Takeout import's notice opens the user's own import
The import's notice reaches a user who may not see the Scheduler, an admin page, yet its link opened it. It now opens Settings › Account, where the import's status is; any other run still opens the Scheduler. The job id is held to the server's by the backend-ids contract. The notice's Hungarian names the list as the app does elsewhere.
This commit is contained in:
@@ -305,6 +305,14 @@ describe("backend ids have their names in both languages", () => {
|
||||
expect([...RUN_STATUSES].sort()).toEqual([...runStatuses].sort());
|
||||
});
|
||||
|
||||
it("the Takeout import's notice is known by the server's job id", () => {
|
||||
// Read as text: the slice pulls in the request layer, which needs a browser.
|
||||
const feedTs = readFileSync(new URL("../lib/api/feed.ts", import.meta.url), "utf8");
|
||||
const ours = /^export const TAKEOUT_JOB = "(\w+)";$/m.exec(feedTs)?.[1];
|
||||
expect(ours).toBeTruthy();
|
||||
expect(/^TAKEOUT_IMPORT = "(\w+)"$/m.exec(schedulerPy)?.[1]).toBe(ours);
|
||||
});
|
||||
|
||||
for (const lang of ["en", "hu"]) {
|
||||
const keys = keysOf(lang);
|
||||
const missing = (wanted: string[]) => wanted.filter((k) => !keys.has(k));
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"openScheduler": "Open Scheduler",
|
||||
"openFile": "Open the file",
|
||||
"openTokens": "Open API tokens",
|
||||
"openTakeout": "Open YouTube history",
|
||||
"revertFailed": "Couldn't undo that — please try again",
|
||||
"downloadDone": {
|
||||
"title": "Download ready"
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"openScheduler": "Ütemező megnyitása",
|
||||
"openFile": "Fájl megnyitása",
|
||||
"openTokens": "API-tokenek megnyitása",
|
||||
"openTakeout": "YouTube-előzmények megnyitása",
|
||||
"revertFailed": "Nem sikerült visszavonni — próbáld újra",
|
||||
"downloadDone": {
|
||||
"title": "Letöltés kész"
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
"tmdb_enrich": "A tükrözött Plex-katalógust dúsítja a TMDB-ből (kulcsszavak, stáb, költségvetés/bevétel, TMDb-pontszám, ajánlások, artwork), a címek kanonikus azonosítója alapján párosítva, és az app saját adatbázisából szolgálja ki. Opcionális — TMDB API-kulcs nélkül nem csinál semmit. Ha leáll, az új Plex-címek csak a következő futásig maradnak TMDB-extrák nélkül.",
|
||||
"people_enrich": "Helyivé teszi a Plex-könyvtár szereplőit: a TMDB-ből letölti az életrajzukat, a portréjukat pedig a Plexszel kicsinyítve elmenti, a legtöbbet szereplőkkel kezdve. A személyoldal és a szereplősáv így az app saját adataiból nyílik meg. A TMDB-rész TMDB API-kulcs nélkül nem csinál semmit, a portrérész Plex nélkül. Ha leáll, a személyoldal megnyitáskor maga kéri le az életrajzot, egy pillanattal később.",
|
||||
"channel_backfill": "Ha egy csatornánál bekapcsolod a teljes előzményt, a teljes korábbi kínálata egyszerre, saját futásként töltődik le. Amit ez nem tud befejezni (egy nagyon nagy csatorna, kevés kvóta, vagy ha már két ilyen fut), azt a „Videók behúzása” feladat fejezi be.",
|
||||
"takeout_import": "Egy felhasználó Google Takeout importja: amit a YouTube-on megnézett, itt is megnézett lesz (a Shorts nem, és amit itt beállított, azt nem írja felül), a YouTube Watch later listája pedig bekerül az övébe. A katalógusban még nem szereplő videó vár, és amint megérkezik, megnézett lesz."
|
||||
"takeout_import": "Egy felhasználó Google Takeout importja: amit a YouTube-on megnézett, itt is megnézett lesz (a Shorts nem, és amit itt beállított, azt nem írja felül), a YouTube Watch later listája pedig bekerül a „Megnézem később” listájába. A katalógusban még nem szereplő videó vár, és amint megérkezik, megnézett lesz."
|
||||
},
|
||||
"jobs": {
|
||||
"rss_poll": "RSS-lekérdezés (új feltöltések)",
|
||||
@@ -183,10 +183,10 @@
|
||||
"cached": "elmentve",
|
||||
"kept": "itteni állapot megtartva",
|
||||
"waiting": "a videójára vár",
|
||||
"watch_later_added": "Watch laterbe téve",
|
||||
"watch_later_present": "már a Watch laterben",
|
||||
"watch_later_added": "a „Megnézem később” listába téve",
|
||||
"watch_later_present": "már a „Megnézem később” listán",
|
||||
"watch_later_missing": "már nincs a YouTube-on",
|
||||
"watch_later_waiting": "Watch later vár",
|
||||
"watch_later_waiting": "a „Megnézem később” listára vár",
|
||||
"takeout_watched": "Takeoutból megnézettre jelölve"
|
||||
},
|
||||
"skip": {
|
||||
|
||||
@@ -20,6 +20,7 @@ import { setupApi } from "./api/setup";
|
||||
export type * from "./api/setup";
|
||||
import { feedApi } from "./api/feed";
|
||||
export type * from "./api/feed";
|
||||
export { TAKEOUT_JOB } from "./api/feed";
|
||||
import { discoverApi } from "./api/discover";
|
||||
export type * from "./api/discover";
|
||||
export { DISCOVER_BUDGETS, HERO_TIERS, discoverCardKey } from "./api/discover";
|
||||
|
||||
@@ -149,6 +149,10 @@ function feedQuery(f: FeedFilters, cursor: string | null, limit: number): string
|
||||
return p.toString();
|
||||
}
|
||||
|
||||
// The import's job (`scheduler.TAKEOUT_IMPORT`, held to it by backendIds.contract.test): its notice
|
||||
// reaches a user who may not see the Scheduler, so it opens their own import instead.
|
||||
export const TAKEOUT_JOB = "takeout_import";
|
||||
|
||||
// A Google Takeout file, uploaded: which one it was, the videos read from it, and its entries left
|
||||
// out (an ad, a view of something that is not a video, a row that could not be read).
|
||||
export interface TakeoutUpload {
|
||||
|
||||
@@ -205,6 +205,34 @@ describe("NotificationsPanel · a server row", () => {
|
||||
expect(menuRows()).toEqual(["inbox.openTokens", "inbox.markRead", "inbox.dismiss"]);
|
||||
});
|
||||
|
||||
it("a Takeout import's run opens the user's own import, any other run the Scheduler", async () => {
|
||||
const run = (id: number, job: string) =>
|
||||
server(id, {
|
||||
type: "scheduler",
|
||||
data: { job_id: job, status: "ok", summary: `${job} done` },
|
||||
});
|
||||
h.server = [run(1, "takeout_import"), run(2, "channel_backfill")];
|
||||
h.client = [];
|
||||
mount();
|
||||
fireEvent.click(screen.getByText("inbox.openTakeout"));
|
||||
expect(h.focusSettingsTab).toHaveBeenCalledWith("account");
|
||||
expect(h.setPage).toHaveBeenLastCalledWith("settings");
|
||||
fireEvent.click(screen.getByText("inbox.openScheduler"));
|
||||
expect(h.setPage).toHaveBeenLastCalledWith("scheduler");
|
||||
const takeoutRow = screen
|
||||
.getByText("inbox.openTakeout")
|
||||
.closest<HTMLElement>("[data-nav-index]")!;
|
||||
await openMenu(() =>
|
||||
fireEvent.click(takeoutRow.querySelector<HTMLElement>('button[aria-label="inbox.rowMenu"]')!),
|
||||
);
|
||||
expect(menuRows()).toEqual(["inbox.openTakeout", "inbox.markRead", "inbox.dismiss"]);
|
||||
h.focusSettingsTab.mockClear();
|
||||
h.setPage.mockClear();
|
||||
choose("inbox.openTakeout");
|
||||
await waitFor(() => expect(h.focusSettingsTab).toHaveBeenCalledWith("account"));
|
||||
expect(h.setPage).toHaveBeenCalledWith("settings");
|
||||
});
|
||||
|
||||
it("holds the link, mark read and dismiss", async () => {
|
||||
h.server = [server(1)];
|
||||
h.client = [];
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
ExternalLink,
|
||||
Eye,
|
||||
FolderOpen,
|
||||
History,
|
||||
KeyRound,
|
||||
RotateCcw,
|
||||
Search,
|
||||
@@ -19,7 +20,7 @@ import {
|
||||
UserPlus,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { api, type AppNotification } from "../../lib/api";
|
||||
import { api, TAKEOUT_JOB, type AppNotification } from "../../lib/api";
|
||||
import { jobRunText } from "../../lib/jobResult";
|
||||
import { notifyActionError } from "../../lib/actionErrors";
|
||||
import { useLiveQuery } from "../../lib/useLiveQuery";
|
||||
@@ -192,6 +193,10 @@ export default function NotificationsPanel() {
|
||||
focusSettingsTab("tokens");
|
||||
setPage("settings");
|
||||
};
|
||||
const openTakeout = () => {
|
||||
focusSettingsTab("account");
|
||||
setPage("settings");
|
||||
};
|
||||
// A finished download opens on its file, in its folder; a notice from before it named one, on Files.
|
||||
const openDone = (n: AppNotification) => {
|
||||
const { job_id: id, folder_id: folder } = n.data ?? {};
|
||||
@@ -213,34 +218,43 @@ export default function NotificationsPanel() {
|
||||
const kind = serverKind(n);
|
||||
return menuGroups(
|
||||
[
|
||||
...(kind === "scheduler"
|
||||
...(kind === "takeout"
|
||||
? [
|
||||
{
|
||||
id: "open",
|
||||
label: t("inbox.openScheduler"),
|
||||
icon: icon(Activity),
|
||||
onSelect: openScheduler,
|
||||
label: t("inbox.openTakeout"),
|
||||
icon: icon(History),
|
||||
onSelect: openTakeout,
|
||||
},
|
||||
]
|
||||
: kind === "download"
|
||||
: kind === "scheduler"
|
||||
? [
|
||||
{
|
||||
id: "open",
|
||||
label: t("inbox.openFile"),
|
||||
icon: icon(FolderOpen),
|
||||
onSelect: () => openDone(n),
|
||||
label: t("inbox.openScheduler"),
|
||||
icon: icon(Activity),
|
||||
onSelect: openScheduler,
|
||||
},
|
||||
]
|
||||
: kind === "token"
|
||||
: kind === "download"
|
||||
? [
|
||||
{
|
||||
id: "open",
|
||||
label: t("inbox.openTokens"),
|
||||
icon: icon(KeyRound),
|
||||
onSelect: openTokens,
|
||||
label: t("inbox.openFile"),
|
||||
icon: icon(FolderOpen),
|
||||
onSelect: () => openDone(n),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
: kind === "token"
|
||||
? [
|
||||
{
|
||||
id: "open",
|
||||
label: t("inbox.openTokens"),
|
||||
icon: icon(KeyRound),
|
||||
onSelect: openTokens,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(!n.read
|
||||
? [
|
||||
{
|
||||
@@ -411,6 +425,7 @@ export default function NotificationsPanel() {
|
||||
onRead={() => readMut.mutate(n.id)}
|
||||
onDismiss={() => dismissMut.mutate(n.id)}
|
||||
onOpenScheduler={openScheduler}
|
||||
onOpenTakeout={openTakeout}
|
||||
onOpenFile={() => openDone(n)}
|
||||
onOpenTokens={openTokens}
|
||||
/>
|
||||
@@ -478,7 +493,9 @@ const DONE_TITLE: Record<string, string | undefined> = {
|
||||
upload: "inbox.uploadDone.title",
|
||||
};
|
||||
|
||||
function serverKind(n: AppNotification): "scheduler" | "download" | "token" | null {
|
||||
function serverKind(n: AppNotification): "scheduler" | "takeout" | "download" | "token" | null {
|
||||
// A Takeout import's run is the user's own: told like any run, opened where they started it.
|
||||
if (n.type === "scheduler" && n.data?.job_id === TAKEOUT_JOB) return "takeout";
|
||||
if (n.type === "scheduler" && typeof n.data?.job_id === "string") return "scheduler";
|
||||
if (n.type === "download_done") return "download";
|
||||
if (n.type === "api_token_expiring" && typeof n.data?.name === "string") return "token";
|
||||
@@ -527,6 +544,7 @@ function NotificationRow({
|
||||
onRead,
|
||||
onDismiss,
|
||||
onOpenScheduler,
|
||||
onOpenTakeout,
|
||||
onOpenFile,
|
||||
onOpenTokens,
|
||||
}: {
|
||||
@@ -538,6 +556,7 @@ function NotificationRow({
|
||||
onRead: () => void;
|
||||
onDismiss: () => void;
|
||||
onOpenScheduler: () => void;
|
||||
onOpenTakeout: () => void;
|
||||
onOpenFile: () => void;
|
||||
onOpenTokens: () => void;
|
||||
}) {
|
||||
@@ -549,7 +568,8 @@ function NotificationRow({
|
||||
// Known notification types are rendered from i18n (so they're trilingual) using the typed
|
||||
// payload; the server-stored title/body are an English fallback for any unknown type.
|
||||
const isMaintenance = n.type === "maintenance" && typeof n.data?.count === "number";
|
||||
const isScheduler = serverKind(n) === "scheduler";
|
||||
const isTakeout = serverKind(n) === "takeout";
|
||||
const isScheduler = serverKind(n) === "scheduler" || isTakeout;
|
||||
const isDownloadDone = serverKind(n) === "download";
|
||||
const isTokenExpiring = serverKind(n) === "token";
|
||||
const isEmailUndelivered = n.type === "email_undelivered" && typeof n.data?.reason === "string";
|
||||
@@ -633,10 +653,12 @@ function NotificationRow({
|
||||
variant="link"
|
||||
className="text-sm mt-1.5"
|
||||
data-nav-open=""
|
||||
onClick={onOpenScheduler}
|
||||
icon={<Activity className="w-3.5 h-3.5" />}
|
||||
onClick={isTakeout ? onOpenTakeout : onOpenScheduler}
|
||||
icon={
|
||||
isTakeout ? <History className="w-3.5 h-3.5" /> : <Activity className="w-3.5 h-3.5" />
|
||||
}
|
||||
>
|
||||
{t("inbox.openScheduler")}
|
||||
{t(isTakeout ? "inbox.openTakeout" : "inbox.openScheduler")}
|
||||
</Button>
|
||||
)}
|
||||
{isTokenExpiring && (
|
||||
|
||||
Reference in New Issue
Block a user