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.", "unverifiedHint": "Installed from a file. Termix did not check or sign it.",
"reportIssue": "Report an issue", "reportIssue": "Report an issue",
"reportTo": "Report to {{author}}", "reportTo": "Report to {{author}}",
"requestFeature": "Request a feature",
"openSettings": "Open its settings" "openSettings": "Open its settings"
} }
}, },
+17
View File
@@ -6,6 +6,7 @@ import {
Database, Database,
Download, Download,
ExternalLink, ExternalLink,
Lightbulb,
Pin, Pin,
RotateCcw, RotateCcw,
Settings, Settings,
@@ -35,6 +36,7 @@ import {
formatBytes, formatBytes,
orderByRisk, orderByRisk,
reportIssueUrl, reportIssueUrl,
requestFeatureUrl,
type PluginEntry, type PluginEntry,
} from "./plugin-model"; } from "./plugin-model";
import { openPluginSettings } from "./open-plugins"; import { openPluginSettings } from "./open-plugins";
@@ -77,6 +79,7 @@ export function PluginDetail({
plugin, plugin,
import.meta.env.VITE_APP_VERSION || undefined, import.meta.env.VITE_APP_VERSION || undefined,
); );
const featureUrl = requestFeatureUrl(plugin);
return ( return (
<div <div
@@ -398,6 +401,20 @@ export function PluginDetail({
</a> </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 && ( {plugin.repository && (
<a <a
href={plugin.repository} 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 * A new bug report on the plugin's own repo with what a maintainer asks first
* first already filled in. Official plugins use the org bug form; a third * already filled in. Official plugins use the org bug form and the plugin
* party repo may not have it, so it gets a plain prefilled body. Null when * template ships the same one. Null when the repository is not on GitHub.
* the repository is not on GitHub.
*/ */
export function reportIssueUrl( export function reportIssueUrl(
plugin: Pick< plugin: Pick<
@@ -347,38 +346,25 @@ export function reportIssueUrl(
if (!repo) return null; if (!repo) return null;
const version = plugin.version ?? plugin.latestVersion ?? "not installed"; const version = plugin.version ?? plugin.latestVersion ?? "not installed";
const state = `Plugin ${plugin.id}, ${plugin.source}, ${plugin.status ?? "not installed"}`; const state = `Plugin ${plugin.id}, ${plugin.source}, ${plugin.status ?? "not installed"}`;
if (plugin.source !== "unverified") { return buildIssueUrl(repo, {
return buildIssueUrl(repo, { template: "bug_report.yml",
template: "bug_report.yml", fields: {
fields: { "termix-version": termixVersion,
"termix-version": termixVersion, "plugin-version": version,
"plugin-version": version, environment: `${describeEnvironment()}
environment: `${describeEnvironment()}\n${state}`, ${state}`,
logs: plugin.lastError, 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 `${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. */ /** One version in a plugin's release notes. */
@@ -99,6 +99,9 @@ describe("PluginsStep", () => {
/>, />,
); );
await screen.findByText("TERM"); 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 () => { it("groups plugins by category and puts consent plugins in their own section", async () => {
+28 -10
View File
@@ -13,6 +13,7 @@ import {
pluginSource, pluginSource,
pluginStatus, pluginStatus,
reportIssueUrl, reportIssueUrl,
requestFeatureUrl,
sortPlugins, sortPlugins,
uploadEntry, uploadEntry,
youtubeEmbedUrl, youtubeEmbedUrl,
@@ -289,19 +290,16 @@ describe("reportIssueUrl", () => {
expect(parsed.searchParams.has("body")).toBe(false); 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( const parsed = new URL(
reportIssueUrl({ ...base, source: "unverified" }, "26.10.0")!, reportIssueUrl({ ...base, source: "unverified" }, "26.10.0")!,
); );
expect(parsed.searchParams.has("template")).toBe(false); expect(parsed.searchParams.get("template")).toBe("bug_report.yml");
expect(parsed.searchParams.get("title")).toBe("[docker] "); expect(parsed.searchParams.get("plugin-version")).toBe("1.0.0");
const body = parsed.searchParams.get("body")!; expect(parsed.searchParams.get("environment")).toContain(
expect(body).toContain("**Plugin:** docker"); "Plugin docker, unverified, failed",
expect(body).toContain("**Version:** 1.0.0"); );
expect(body).toContain("**Source:** unverified"); expect(parsed.searchParams.has("body")).toBe(false);
expect(body).toContain("**State:** failed");
expect(body).toContain("**Termix:** 26.10.0");
expect(body).toContain("boom");
}); });
it("gives nothing for a repo that is not on GitHub", () => { 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", () => { describe("mergeReleaseNotes", () => {
const registryVersion = (version: string, extra = {}) => ({ const registryVersion = (version: string, extra = {}) => ({
version, version,