mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-30 07:05:24 +00:00
feat: show only the latest plugin release notes and cap long ones
This commit is contained in:
1 parent
1d3e77227c
commit
7804d4cc8d
3 files changed
+99
-22
No files matched your search
@@ -2108,8 +2108,9 @@
|
|||||||
"releaseNotes": "Release notes",
|
"releaseNotes": "Release notes",
|
||||||
"noReleaseNotes": "No notes for this version.",
|
"noReleaseNotes": "No notes for this version.",
|
||||||
"viewRelease": "View on GitHub",
|
"viewRelease": "View on GitHub",
|
||||||
"showAllReleases": "Show all {{count}}",
|
"showOlderReleases": "Older releases ({{count}})",
|
||||||
"showFewerReleases": "Show fewer",
|
"hideOlderReleases": "Hide older releases",
|
||||||
|
"showMoreChanges": "Show {{count}} more",
|
||||||
"videoTitle": "{{name}} video",
|
"videoTitle": "{{name}} video",
|
||||||
"changeTypes": {
|
"changeTypes": {
|
||||||
"added": "Added",
|
"added": "Added",
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ import {
|
|||||||
type ReleaseRow,
|
type ReleaseRow,
|
||||||
} from "./plugin-model";
|
} from "./plugin-model";
|
||||||
|
|
||||||
const SHOWN = 3;
|
/** A long release shows this many changes until it is opened up. */
|
||||||
|
const MAX_CHANGES = 8;
|
||||||
|
|
||||||
function formatDate(value: string | null): string {
|
function formatDate(value: string | null): string {
|
||||||
if (!value) return "";
|
if (!value) return "";
|
||||||
@@ -48,7 +49,17 @@ function Release({
|
|||||||
installedVersion: string | null;
|
installedVersion: string | null;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const groups = groupChanges(row.changes);
|
const [full, setFull] = useState(false);
|
||||||
|
const all = groupChanges(row.changes);
|
||||||
|
const hidden = full ? 0 : Math.max(0, row.changes.length - MAX_CHANGES);
|
||||||
|
let budget = full ? Infinity : MAX_CHANGES;
|
||||||
|
const groups = all
|
||||||
|
.map((group) => {
|
||||||
|
const items = group.items.slice(0, Math.max(0, budget));
|
||||||
|
budget -= items.length;
|
||||||
|
return { ...group, items };
|
||||||
|
})
|
||||||
|
.filter((group) => group.items.length > 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2 px-4 py-3">
|
<div className="flex flex-col gap-2 px-4 py-3">
|
||||||
@@ -69,7 +80,9 @@ function Release({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{row.summary && (
|
{row.summary && (
|
||||||
<p className="whitespace-pre-line text-[11px] leading-relaxed text-muted-foreground">
|
<p
|
||||||
|
className={`whitespace-pre-line text-[11px] leading-relaxed text-muted-foreground ${full ? "" : "line-clamp-4"}`}
|
||||||
|
>
|
||||||
<InlineText text={row.summary} />
|
<InlineText text={row.summary} />
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -87,6 +100,15 @@ function Release({
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
{hidden > 0 && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setFull(true)}
|
||||||
|
className="w-fit text-[11px] text-muted-foreground hover:text-accent-brand"
|
||||||
|
>
|
||||||
|
{t("plugins.manager.showMoreChanges", { count: hidden })}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
{groups.length === 0 && !row.summary && (
|
{groups.length === 0 && !row.summary && (
|
||||||
<span className="text-[11px] text-muted-foreground">
|
<span className="text-[11px] text-muted-foreground">
|
||||||
{t("plugins.manager.noReleaseNotes")}
|
{t("plugins.manager.noReleaseNotes")}
|
||||||
@@ -107,13 +129,14 @@ function Release({
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Every version's notes, from the registry and the installed CHANGELOG.md.
|
* The newest version's notes, from the registry and the installed
|
||||||
|
* CHANGELOG.md, with older versions behind a toggle in a capped scroll area.
|
||||||
* Renders nothing when there are no versions to list.
|
* Renders nothing when there are no versions to list.
|
||||||
*/
|
*/
|
||||||
export function PluginReleaseNotes({ plugin }: { plugin: PluginEntry }) {
|
export function PluginReleaseNotes({ plugin }: { plugin: PluginEntry }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [local, setLocal] = useState<ChangelogRelease[]>([]);
|
const [local, setLocal] = useState<ChangelogRelease[]>([]);
|
||||||
const [expanded, setExpanded] = useState(false);
|
const [showOlder, setShowOlder] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
@@ -134,35 +157,42 @@ export function PluginReleaseNotes({ plugin }: { plugin: PluginEntry }) {
|
|||||||
[plugin.versions, local],
|
[plugin.versions, local],
|
||||||
);
|
);
|
||||||
if (rows.length === 0) return null;
|
if (rows.length === 0) return null;
|
||||||
const shown = expanded ? rows : rows.slice(0, SHOWN);
|
const [latest, ...older] = rows;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionCard
|
<SectionCard
|
||||||
title={t("plugins.manager.releaseNotes")}
|
title={t("plugins.manager.releaseNotes")}
|
||||||
icon={null}
|
icon={null}
|
||||||
action={
|
action={
|
||||||
rows.length > SHOWN ? (
|
older.length > 0 ? (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="xs"
|
size="xs"
|
||||||
className="text-muted-foreground"
|
className="text-muted-foreground"
|
||||||
onClick={() => setExpanded((v) => !v)}
|
onClick={() => setShowOlder((v) => !v)}
|
||||||
>
|
>
|
||||||
{expanded
|
{showOlder
|
||||||
? t("plugins.manager.showFewerReleases")
|
? t("plugins.manager.hideOlderReleases")
|
||||||
: t("plugins.manager.showAllReleases", { count: rows.length })}
|
: t("plugins.manager.showOlderReleases", {
|
||||||
|
count: older.length,
|
||||||
|
})}
|
||||||
</Button>
|
</Button>
|
||||||
) : undefined
|
) : undefined
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="-mx-3 -my-1 divide-y divide-border md:-mx-4">
|
<div className="-mx-3 -my-1 md:-mx-4">
|
||||||
{shown.map((row) => (
|
<Release row={latest} installedVersion={plugin.version} />
|
||||||
<Release
|
{showOlder && older.length > 0 && (
|
||||||
key={row.version}
|
<div className="max-h-96 divide-y divide-border overflow-y-auto border-t border-border">
|
||||||
row={row}
|
{older.map((row) => (
|
||||||
installedVersion={plugin.version}
|
<Release
|
||||||
/>
|
key={row.version}
|
||||||
))}
|
row={row}
|
||||||
|
installedVersion={plugin.version}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</SectionCard>
|
</SectionCard>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
import { cleanup, render, screen, waitFor } from "@testing-library/react";
|
import {
|
||||||
|
cleanup,
|
||||||
|
fireEvent,
|
||||||
|
render,
|
||||||
|
screen,
|
||||||
|
waitFor,
|
||||||
|
} from "@testing-library/react";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
import { I18nextProvider, initReactI18next } from "react-i18next";
|
import { I18nextProvider, initReactI18next } from "react-i18next";
|
||||||
import type { PluginEntry } from "@/plugins/plugin-model";
|
import type { PluginEntry } from "@/plugins/plugin-model";
|
||||||
@@ -27,6 +33,9 @@ async function i18n() {
|
|||||||
releaseNotes: "Release notes",
|
releaseNotes: "Release notes",
|
||||||
installedBadge: "Installed",
|
installedBadge: "Installed",
|
||||||
changeTypes: { added: "Added", fixed: "Fixed" },
|
changeTypes: { added: "Added", fixed: "Fixed" },
|
||||||
|
showOlderReleases: "Older releases ({{count}})",
|
||||||
|
hideOlderReleases: "Hide older releases",
|
||||||
|
showMoreChanges: "Show {{count}} more",
|
||||||
videoTitle: "{{name}} video",
|
videoTitle: "{{name}} video",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -71,6 +80,43 @@ describe("PluginReleaseNotes", () => {
|
|||||||
expect(screen.getByText("Installed")).toBeTruthy();
|
expect(screen.getByText("Installed")).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("shows only the newest release until older ones are opened", async () => {
|
||||||
|
getPluginChangelog.mockResolvedValue([
|
||||||
|
{ version: "1.1.0", changes: [{ type: "fixed", text: "Newest" }] },
|
||||||
|
{ version: "1.0.0", changes: [{ type: "added", text: "Oldest" }] },
|
||||||
|
]);
|
||||||
|
render(
|
||||||
|
<I18nextProvider i18n={await i18n()}>
|
||||||
|
<PluginReleaseNotes plugin={plugin({ version: "1.1.0" })} />
|
||||||
|
</I18nextProvider>,
|
||||||
|
);
|
||||||
|
await waitFor(() => expect(screen.getByText("Newest")).toBeTruthy());
|
||||||
|
expect(screen.queryByText("Oldest")).toBeNull();
|
||||||
|
fireEvent.click(screen.getByText("Older releases (1)"));
|
||||||
|
expect(screen.getByText("Oldest")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("cuts a long release down until it is opened up", async () => {
|
||||||
|
getPluginChangelog.mockResolvedValue([
|
||||||
|
{
|
||||||
|
version: "1.0.0",
|
||||||
|
changes: Array.from({ length: 12 }, (_, i) => ({
|
||||||
|
type: "added",
|
||||||
|
text: `Change ${i}`,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
render(
|
||||||
|
<I18nextProvider i18n={await i18n()}>
|
||||||
|
<PluginReleaseNotes plugin={plugin()} />
|
||||||
|
</I18nextProvider>,
|
||||||
|
);
|
||||||
|
await waitFor(() => expect(screen.getByText("Change 7")).toBeTruthy());
|
||||||
|
expect(screen.queryByText("Change 8")).toBeNull();
|
||||||
|
fireEvent.click(screen.getByText("Show 4 more"));
|
||||||
|
expect(screen.getByText("Change 11")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
it("renders nothing for a plugin with no versions", async () => {
|
it("renders nothing for a plugin with no versions", async () => {
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<I18nextProvider i18n={await i18n()}>
|
<I18nextProvider i18n={await i18n()}>
|
||||||
|
|||||||
Reference in new issue
Block a user