diff --git a/src/ui/locales/en.json b/src/ui/locales/en.json
index 50c3e0f3a..374b130f4 100644
--- a/src/ui/locales/en.json
+++ b/src/ui/locales/en.json
@@ -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"
}
},
diff --git a/src/ui/plugins/PluginDetail.tsx b/src/ui/plugins/PluginDetail.tsx
index 3634bb9a0..b3361de6d 100644
--- a/src/ui/plugins/PluginDetail.tsx
+++ b/src/ui/plugins/PluginDetail.tsx
@@ -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 (
)}
+ {featureUrl && (
+
+
+
+ {t("plugins.manager.requestFeature")}
+
+
+ )}
+
{plugin.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. */
diff --git a/src/ui/tests/onboarding/steps/PluginsStep.test.tsx b/src/ui/tests/onboarding/steps/PluginsStep.test.tsx
index d4836123e..3958011ad 100644
--- a/src/ui/tests/onboarding/steps/PluginsStep.test.tsx
+++ b/src/ui/tests/onboarding/steps/PluginsStep.test.tsx
@@ -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 () => {
diff --git a/src/ui/tests/plugins/plugin-model.test.ts b/src/ui/tests/plugins/plugin-model.test.ts
index 7d2ec35d2..7ae35b1f5 100644
--- a/src/ui/tests/plugins/plugin-model.test.ts
+++ b/src/ui/tests/plugins/plugin-model.test.ts
@@ -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,