Skip to content

Commit 0bfcaa3

Browse files
feat: in-app changelog (#309)
* feat: add in-app changelog - Implement useChangelog hook to fetch GitHub releases - Create ChangelogDialog with current version filtering - Add interactive markdown parsing for links, bolds, PRs, and users * test: add changelog dialog tests Add Vitest/RTL coverage for changelog dialog states and link behaviors. * fix: prevent double Escape in dialogs Ensure AboutDialog only listens for Escape in the about view, wire ChangelogDialog Escape to go back to About, remove the redundant Close button in the changelog header, and update dialog tests to match the new navigation behavior. * fix: prevent crash on null changelog markdown * fix: avoid nested buttons in changelog links * fix: use UTC date for changelog release * fix: harden changelog dialog for null fields * feat: fetch only current version in use-changelog * feat: support autolinking plain URLs in changelog * fix: clear stale changelog on refetch * fix: improve plain URL auto-linking detection * test: mock useChangelog in about-dialog.test.tsx * fix: preserve ? and () in plain URL auto-linking --------- Co-authored-by: Robin Ebers <robin.ebers@gmail.com>
1 parent 73fe349 commit 0bfcaa3

6 files changed

Lines changed: 848 additions & 5 deletions

File tree

src/components/about-dialog.test.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,20 @@ const openerState = vi.hoisted(() => ({
88
openUrlMock: vi.fn(() => Promise.resolve()),
99
}))
1010

11+
const changelogState = vi.hoisted(() => ({
12+
releases: [] as import("@/hooks/use-changelog").Release[],
13+
loading: false,
14+
error: null as string | null,
15+
}))
16+
1117
vi.mock("@tauri-apps/plugin-opener", () => ({
1218
openUrl: openerState.openUrlMock,
1319
}))
1420

21+
vi.mock("@/hooks/use-changelog", () => ({
22+
useChangelog: () => changelogState,
23+
}))
24+
1525
describe("AboutDialog", () => {
1626
it("renders version, links, and maintainers", () => {
1727
render(<AboutDialog version="1.2.3" onClose={() => {}} />)
@@ -40,6 +50,20 @@ describe("AboutDialog", () => {
4050
expect(onClose).toHaveBeenCalled()
4151
})
4252

53+
it("goes back to about view on Escape when showing changelog", async () => {
54+
const onClose = vi.fn()
55+
render(<AboutDialog version="1.2.3" onClose={onClose} />)
56+
57+
// Switch to changelog view.
58+
await userEvent.click(screen.getByRole("button", { name: "View Changelog" }))
59+
60+
// Press Escape; should go back to About view, not close.
61+
await userEvent.keyboard("{Escape}")
62+
63+
expect(onClose).not.toHaveBeenCalled()
64+
expect(screen.getByText("OpenUsage")).toBeInTheDocument()
65+
})
66+
4367
it("does not close on other keys", async () => {
4468
const onClose = vi.fn()
4569
render(<AboutDialog version="1.2.3" onClose={onClose} />)

src/components/about-dialog.tsx

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
import { useEffect } from "react";
1+
import { useEffect, useState } from "react";
22
import { openUrl } from "@tauri-apps/plugin-opener";
3+
import { ChangelogDialog } from "./changelog-dialog";
4+
import { Button } from "@/components/ui/button";
35

46
interface AboutDialogProps {
57
version: string;
@@ -29,8 +31,14 @@ function ExternalLink({
2931
}
3032

3133
export function AboutDialog({ version, onClose }: AboutDialogProps) {
34+
const [view, setView] = useState<"about" | "changelog">("about");
35+
3236
// Close on ESC key
3337
useEffect(() => {
38+
if (view !== "about") {
39+
return;
40+
}
41+
3442
const handleKeyDown = (e: KeyboardEvent) => {
3543
if (e.key === "Escape") {
3644
e.preventDefault();
@@ -39,7 +47,7 @@ export function AboutDialog({ version, onClose }: AboutDialogProps) {
3947
};
4048
document.addEventListener("keydown", handleKeyDown);
4149
return () => document.removeEventListener("keydown", handleKeyDown);
42-
}, [onClose]);
50+
}, [onClose, view]);
4351

4452
// Close when panel hides (loses visibility)
4553
useEffect(() => {
@@ -59,6 +67,18 @@ export function AboutDialog({ version, onClose }: AboutDialogProps) {
5967
}
6068
};
6169

70+
if (view === "changelog") {
71+
return (
72+
<ChangelogDialog
73+
currentVersion={version}
74+
onBack={() => setView("about")}
75+
// In changelog view, Escape should go back to About instead of
76+
// closing the entire dialog, so hand off to setView.
77+
onClose={() => setView("about")}
78+
/>
79+
);
80+
}
81+
6282
return (
6383
<div
6484
className="absolute inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm rounded-xl"
@@ -73,9 +93,19 @@ export function AboutDialog({ version, onClose }: AboutDialogProps) {
7393

7494
<h2 className="text-xl font-semibold mb-1">OpenUsage</h2>
7595

76-
<span className="inline-block text-xs text-muted-foreground bg-muted px-2 py-0.5 rounded-full mb-4">
77-
v{version}
78-
</span>
96+
<div className="flex flex-col items-center gap-2 mb-4">
97+
<span className="inline-block text-xs text-muted-foreground bg-muted px-2 py-0.5 rounded-full">
98+
v{version}
99+
</span>
100+
<Button
101+
size="xs"
102+
variant="outline"
103+
onClick={() => setView("changelog")}
104+
className="text-[10px] h-5 px-1.5"
105+
>
106+
View Changelog
107+
</Button>
108+
</div>
79109

80110
<div className="text-sm text-muted-foreground space-y-1">
81111
<p>
@@ -103,3 +133,4 @@ export function AboutDialog({ version, onClose }: AboutDialogProps) {
103133
</div>
104134
);
105135
}
136+

0 commit comments

Comments
 (0)