feat: show plugin features from the manifest on the plugin page

This commit is contained in:
LukeGus committed 2026-10-06 22:38:13 -05:00
1 parent 60024bdd4e
commit b6bb3ffc73
13 files changed
+158 -2

No files matched your search

+4
View File
@@ -252,6 +252,8 @@ export interface PluginSummary {
repository?: string;
/** A YouTube video id from the manifest. Admins only. */
videoId?: string;
/** What the plugin does, from the manifest. Admins only. */
features?: string[];
signedBy?: string | null;
}
@@ -286,6 +288,7 @@ export interface RegistryPluginEntry {
repository?: string;
icon?: string;
videoId?: string;
features?: string[];
versions: RegistryPluginVersion[];
latestVersion: string | null;
installed: boolean;
@@ -499,6 +502,7 @@ export interface PluginUploadPreview {
version: string;
description: string;
author: string;
features?: string[];
capabilities: string[];
replaces: string | null;
}
+1
View File
@@ -2122,6 +2122,7 @@
},
"installedBadge": "Installed",
"incompatible": "Needs a newer Termix",
"features": "Features",
"whatItCanDo": "What it can do",
"addsToTermix": "Adds to Termix",
"addsToTermixHint": "Uninstall and every one of these goes away.",
+17
View File
@@ -2,6 +2,7 @@ import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import {
Bug,
Check,
Database,
Download,
ExternalLink,
@@ -203,6 +204,22 @@ export function PluginDetail({
<div className="flex flex-col gap-2 lg:col-span-2">
<PluginVideo videoId={plugin.videoId} name={plugin.name} />
{plugin.features.length > 0 && (
<SectionCard title={t("plugins.manager.features")} icon={null}>
<ul className="grid grid-cols-1 gap-x-4 gap-y-1.5 px-4 py-3 sm:grid-cols-2">
{plugin.features.map((feature) => (
<li
key={feature}
className="flex items-start gap-2 text-xs leading-snug"
>
<Check className="mt-px size-3.5 shrink-0 text-accent-brand" />
<span>{feature}</span>
</li>
))}
</ul>
</SectionCard>
)}
<SectionCard title={t("plugins.manager.whatItCanDo")} icon={null}>
<div className="divide-y divide-border">
{orderByRisk(plugin.capabilities).map((capability) => (
+7
View File
@@ -34,6 +34,7 @@ export interface PluginEntry {
icon?: string;
repository?: string;
videoId?: string;
features: string[];
installed: boolean;
version: string | null;
latestVersion: string | null;
@@ -94,6 +95,9 @@ export function mergePlugins(
icon: summary.icon ?? entry?.icon,
repository: summary.repository ?? entry?.repository,
videoId: summary.videoId ?? entry?.videoId,
features: summary.features?.length
? summary.features
: (entry?.features ?? []),
installed: true,
version: summary.version,
latestVersion: entry?.latestVersion ?? null,
@@ -130,6 +134,7 @@ export function mergePlugins(
icon: entry.icon,
repository: entry.repository,
videoId: entry.videoId,
features: entry.features ?? [],
installed: false,
version: null,
latestVersion: entry.latestVersion,
@@ -163,6 +168,7 @@ export function uploadEntry(preview: PluginUploadPreview): PluginEntry {
name: preview.name,
description: preview.description,
author: preview.author,
features: preview.features ?? [],
category: "",
installed: preview.replaces !== null,
version: preview.replaces,
@@ -226,6 +232,7 @@ export function matchesQuery(row: PluginEntry, query: string): boolean {
return (
row.name.toLowerCase().includes(q) ||
row.description.toLowerCase().includes(q) ||
row.features.some((f) => f.toLowerCase().includes(q)) ||
row.id.includes(q)
);
}
+16
View File
@@ -92,6 +92,18 @@ describe("mergePlugins", () => {
});
});
it("uses the installed features and falls back to the registry", () => {
const registry = entry({ features: ["From registry"] });
expect(
mergePlugins([summary({ features: ["Installed"] })], [registry])[0]
.features,
).toEqual(["Installed"]);
expect(mergePlugins([summary()], [registry])[0].features).toEqual([
"From registry",
]);
expect(mergePlugins([summary()], null)[0].features).toEqual([]);
});
it("keeps installed plugins when the registry is unreachable", () => {
const rows = mergePlugins([summary()], null);
expect(rows).toHaveLength(1);
@@ -120,6 +132,7 @@ describe("sorting and filtering", () => {
name: "Alpha",
category: "Access",
description: "Sign in",
features: ["Works with Podman"],
versions: [
{
version: "1.0.0",
@@ -143,6 +156,9 @@ describe("sorting and filtering", () => {
expect(
rows.filter((r) => matchesQuery(r, "sign")).map((r) => r.id),
).toEqual(["a"]);
expect(
rows.filter((r) => matchesQuery(r, "podman")).map((r) => r.id),
).toEqual(["a"]);
expect(rows.filter((r) => matchesQuery(r, " ")).length).toBe(2);
});