diff --git a/src/features/app/__tests__/updates.test.ts b/src/features/app/__tests__/updates.test.ts index f1710e97..1a4a71f8 100644 --- a/src/features/app/__tests__/updates.test.ts +++ b/src/features/app/__tests__/updates.test.ts @@ -41,6 +41,7 @@ vi.mock("@/platform/ipc", async (orig) => { const { checkForUpdates, installUpdate, notifyUpdate, progressText, CHECK_TASK_ID, INSTALL_TASK_ID, UPDATE_KEY } = await import("../updates"); const { provideNotificationService } = await import("@/workbench/contributions"); +const { useProject } = await import("@/platform/project"); const { useTasks } = await import("@/workbench/tasks"); type Note = Parameters[0]; @@ -59,6 +60,7 @@ beforeEach(() => { check = deferred(); provideNotificationService({ notify: (n) => void notes.push(n), expire: (k) => void expired.push(k) }); useTasks.setState({ tasks: [] }); + useProject.setState({ project: null, lightEdit: false }); }); afterEach(() => vi.clearAllMocks()); @@ -163,6 +165,23 @@ describe("Check for Updates", () => { expect(notes.at(-1)?.title).toBe(title); expect(task(CHECK_TASK_ID)?.done).toBe(true); }); + + it("shows the check in a balloon where there is no status bar (the welcome screen)", async () => { + const done = checkForUpdates(); + expect(notes[0]).toMatchObject({ level: "info", title: "Checking for updates…", key: UPDATE_KEY }); + check.resolve(ok({ enabled: true, update: null })); + await done; + expect(notes.at(-1)).toMatchObject({ title: "OpenIDEA 0.1.21 is up to date", key: UPDATE_KEY }); + }); + + it("leaves the check to the status bar in a project window", async () => { + useProject.setState({ project: { root: "/p", name: "p", trusted: true, isGit: false, settings: {} } as never }); + const done = checkForUpdates(); + expect(notes).toHaveLength(0); + check.resolve(ok({ enabled: true, update: null })); + await done; + expect(notes.at(-1)?.title).toBe("OpenIDEA 0.1.21 is up to date"); + }); }); describe("progressText", () => { diff --git a/src/features/app/updates.tsx b/src/features/app/updates.tsx index 7cdae992..e0451b85 100644 --- a/src/features/app/updates.tsx +++ b/src/features/app/updates.tsx @@ -1,7 +1,7 @@ /** * Update check and install. Both run as background tasks in the status bar: the - * check while the request is out, the install with download progress and Cancel, - * which its balloon shows too (the welcome screen has no status bar). + * check while the request is out, the install with download progress and Cancel. + * Their balloons show the progress too (the welcome screen has no status bar). * Every entry point (Help menu, toolbar gear, welcome screen, Search Everywhere, * the "is available" notification and its log entry) goes through these, so a * repeated click joins the running check or install instead of starting another. @@ -17,6 +17,7 @@ import { type UpdateInfo, type UpdateProgress, } from "@/platform/ipc"; +import { useProject } from "@/platform/project"; import { notifications } from "@/workbench/contributions"; import { upsertTask } from "@/workbench/tasks"; import { UpdateBalloonBody } from "./UpdateBalloonBody"; @@ -45,6 +46,15 @@ async function runCheck() { let abandon = () => {}; const abandoned = new Promise<"cancelled">((resolve) => (abandon = () => resolve("cancelled"))); upsertTask({ id: CHECK_TASK_ID, title, message: null, fraction: null, done: false, cancel: () => abandon() }); + // No status bar (the welcome screen): the balloon is the only place the check can show. + const { project, lightEdit } = useProject.getState(); + if (!installing && !project && !lightEdit) + notifications().notify({ + level: "info", + title, + body: , + key: UPDATE_KEY, + }); try { const r = await Promise.race([call(commands.appCheckUpdate()), abandoned]); if (r === "cancelled") return; diff --git a/src/workbench/activate.ts b/src/workbench/activate.ts index 71ea2b99..2cc8abd9 100644 --- a/src/workbench/activate.ts +++ b/src/workbench/activate.ts @@ -15,7 +15,7 @@ import { installBrowserGuards } from "./browserGuards"; import { installDistractionFree } from "./distractionFree"; import { installFileDrop } from "./fileDrop"; import { installFocusTracking } from "./focus"; -import { clearNotifications, installNotifyEvents, notificationServiceImpl } from "./notificationStore"; +import { clearProjectNotifications, installNotifyEvents, notificationServiceImpl } from "./notificationStore"; import { NOTIFICATIONS_TOOL_WINDOW, NotificationsHeaderActions, @@ -51,7 +51,7 @@ export function activateWorkbench(): () => void { const unsubProject = useProject.subscribe((s, prev) => { if (s.project?.root === prev.project?.root && s.lightEdit === prev.lightEdit) return; // The previous project's notifications (and their actions) no longer apply. - if (prev.project) clearNotifications(); + if (prev.project) clearProjectNotifications(); onProject(s.project?.root ?? null); }); diff --git a/src/workbench/notificationStore.ts b/src/workbench/notificationStore.ts index 5069884a..2a0b4e26 100644 --- a/src/workbench/notificationStore.ts +++ b/src/workbench/notificationStore.ts @@ -175,6 +175,11 @@ export function clearNotifications() { useNotifications.setState({ items: [] }); } +/** A project switch drops the previous project's notifications; app-wide ones (updates) stay. */ +export function clearProjectNotifications() { + useNotifications.setState((s) => ({ items: s.items.filter((x) => x.group === "updates") })); +} + /** The list was seen: everything is read and no longer shown as a balloon. */ export function markAllNotificationsRead() { useNotifications.setState((s) => ({ diff --git a/src/workbench/projectNotifications.test.ts b/src/workbench/projectNotifications.test.ts new file mode 100644 index 00000000..9294cab5 --- /dev/null +++ b/src/workbench/projectNotifications.test.ts @@ -0,0 +1,26 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { useProject } from "@/platform/project"; +import { activateWorkbench } from "./activate"; +import { notify, useNotifications } from "./notificationStore"; + +afterEach(() => { + useNotifications.setState({ items: [] }); + useProject.setState({ project: null, lightEdit: false }); +}); + +describe("notifications across project changes", () => { + it("keeps app-level update notifications and drops project ones", () => { + useProject.setState({ + project: { root: "/p", name: "p", trusted: true, isGit: false, settings: {} } as never, + }); + const dispose = activateWorkbench(); + try { + notify({ level: "info", title: "Downloading OpenIDEA 0.1.35", key: "app.update", sticky: true }); + notify({ level: "info", title: "Git refresh failed", group: "vcs" }); + useProject.setState({ project: null }); + expect(useNotifications.getState().items.map((n) => n.title)).toEqual(["Downloading OpenIDEA 0.1.35"]); + } finally { + dispose(); + } + }); +});