feat: request a feature link for plugins and use the bug form for unverified plugins

This commit is contained in:
LukeGus committed 2026-10-07 10:39:52 -05:00
1 parent 8666533162
commit 6a855c0fa8
5 files changed
+70 -45

No files matched your search

+1
View File
@@ -2167,6 +2167,7 @@
"unverifiedHint": "Installed from a file. Termix did not check or sign it.",
"reportIssue": "Report an issue",
"reportTo": "Report to {{author}}",
"requestFeature": "Request a feature",
"openSettings": "Open its settings"
}
},
+17
View File
@@ -6,6 +6,7 @@ import {
Database,
Download,
ExternalLink,
Lightbulb,
Pin,
RotateCcw,
Settings,
@@ -35,6 +36,7 @@ import {
formatBytes,
orderByRisk,
reportIssueUrl,
requestFeatureUrl,
type PluginEntry,
} from "./plugin-model";
import { openPluginSettings } from "./open-plugins";
@@ -77,6 +79,7 @@ export function PluginDetail({
plugin,
import.meta.env.VITE_APP_VERSION || undefined,
);
const featureUrl = requestFeatureUrl(plugin);
return (
<div
@@ -398,6 +401,20 @@ export function PluginDetail({
</a>
)}
{featureUrl && (
<a
href={featureUrl}
target="_blank"
rel="noreferrer"
className={LINK_ROW}
>
<Lightbulb className="size-3.5 shrink-0" />
<span className="truncate">
{t("plugins.manager.requestFeature")}
</span>
</a>
)}
{plugin.repository && (
<a
href={plugin.repository}
+21 -35
View File
@@ -325,10 +325,9 @@ export function formatCount(count: number): string {
}
/**
* A new GitHub issue on the plugin's own repo with what a maintainer asks
* first already filled in. Official plugins use the org bug form; a third
* party repo may not have it, so it gets a plain prefilled body. Null when
* the repository is not on GitHub.
* A new bug report on the plugin's own repo with what a maintainer asks first
* already filled in. Official plugins use the org bug form and the plugin
* template ships the same one. Null when the repository is not on GitHub.
*/
export function reportIssueUrl(
plugin: Pick<
@@ -347,38 +346,25 @@ export function reportIssueUrl(
if (!repo) return null;
const version = plugin.version ?? plugin.latestVersion ?? "not installed";
const state = `Plugin ${plugin.id}, ${plugin.source}, ${plugin.status ?? "not installed"}`;
if (plugin.source !== "unverified") {
return buildIssueUrl(repo, {
template: "bug_report.yml",
fields: {
"termix-version": termixVersion,
"plugin-version": version,
environment: `${describeEnvironment()}\n${state}`,
logs: plugin.lastError,
},
});
}
const lines = [
`**Plugin:** ${plugin.id}`,
`**Version:** ${version}`,
`**Source:** ${plugin.source}`,
`**State:** ${plugin.status ?? "not installed"}`,
...(termixVersion ? [`**Termix:** ${termixVersion}`] : []),
`**Environment:** ${describeEnvironment()}`,
...(plugin.lastError
? ["", "**Error:**", "```", plugin.lastError, "```"]
: []),
"",
"**What happened:**",
"",
"**What you expected:**",
"",
];
const params = new URLSearchParams({
title: `[${plugin.id}] `,
body: lines.join("\n"),
return buildIssueUrl(repo, {
template: "bug_report.yml",
fields: {
"termix-version": termixVersion,
"plugin-version": version,
environment: `${describeEnvironment()}
${state}`,
logs: plugin.lastError,
},
});
return `${repo}/issues/new?${params.toString()}`;
}
/** A new feature request on the plugin's own repo, or null when not on GitHub. */
export function requestFeatureUrl(
plugin: Pick<PluginEntry, "repository">,
): string | null {
const repo = normalizeGitHubRepo(plugin.repository);
if (!repo) return null;
return buildIssueUrl(repo, { template: "feature_request.yml" });
}
/** One version in a plugin's release notes. */
@@ -99,6 +99,9 @@ describe("PluginsStep", () => {
/>,
);
await screen.findByText("TERM");
// findByText resolves on the DOM change, before the effect that hands
// over the loaded apply has run. Flush it so beforeNext is never stale.
await act(async () => {});
}
it("groups plugins by category and puts consent plugins in their own section", async () => {
+28 -10
View File
@@ -13,6 +13,7 @@ import {
pluginSource,
pluginStatus,
reportIssueUrl,
requestFeatureUrl,
sortPlugins,
uploadEntry,
youtubeEmbedUrl,
@@ -289,19 +290,16 @@ describe("reportIssueUrl", () => {
expect(parsed.searchParams.has("body")).toBe(false);
});
it("uses a plain body for an unverified plugin", () => {
it("fills the same bug form on an unverified plugin repo", () => {
const parsed = new URL(
reportIssueUrl({ ...base, source: "unverified" }, "26.10.0")!,
);
expect(parsed.searchParams.has("template")).toBe(false);
expect(parsed.searchParams.get("title")).toBe("[docker] ");
const body = parsed.searchParams.get("body")!;
expect(body).toContain("**Plugin:** docker");
expect(body).toContain("**Version:** 1.0.0");
expect(body).toContain("**Source:** unverified");
expect(body).toContain("**State:** failed");
expect(body).toContain("**Termix:** 26.10.0");
expect(body).toContain("boom");
expect(parsed.searchParams.get("template")).toBe("bug_report.yml");
expect(parsed.searchParams.get("plugin-version")).toBe("1.0.0");
expect(parsed.searchParams.get("environment")).toContain(
"Plugin docker, unverified, failed",
);
expect(parsed.searchParams.has("body")).toBe(false);
});
it("gives nothing for a repo that is not on GitHub", () => {
@@ -314,6 +312,26 @@ describe("reportIssueUrl", () => {
});
});
describe("requestFeatureUrl", () => {
it("opens the feature form on the plugin repo", () => {
const parsed = new URL(
requestFeatureUrl({
repository: "https://github.com/Termix-SSH/Plugin-Docker/",
})!,
);
expect(parsed.origin + parsed.pathname).toBe(
"https://github.com/Termix-SSH/Plugin-Docker/issues/new",
);
expect(parsed.searchParams.get("template")).toBe("feature_request.yml");
});
it("gives nothing for a repo that is not on GitHub", () => {
expect(
requestFeatureUrl({ repository: "https://gitlab.com/a/b" }),
).toBeNull();
});
});
describe("mergeReleaseNotes", () => {
const registryVersion = (version: string, extra = {}) => ({
version,