refactor: simplify plugin toolbar menu

This commit is contained in:
tianma-if committed 2026-09-09 00:04:01 +08:00
1 parent 6036bb7162
commit 29ec6fe766
7 files changed
+10 -102

No files matched your search

@@ -1,7 +1,6 @@
import { useEffect, useState, useSyncExternalStore } from "react";
import { Clock3, LoaderCircle, PanelRightOpen, Play, Puzzle, Settings2 } from "lucide-react";
import { LoaderCircle, PanelRightOpen, Play, Puzzle, Settings2 } from "lucide-react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
@@ -14,7 +13,7 @@ import {
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { PluginPanelDialog } from "@/components/plugins/PluginPanelDialog";
import { cn } from "@/lib/utils";
import { getPluginDetailPath, getPluginToolbarGroups } from "@/lib/plugins/plugin-navigation";
import { getPluginToolbarGroups } from "@/lib/plugins/plugin-navigation";
import type {
EdgeEverPluginHost,
RegisteredPluginAction,
@@ -40,7 +39,6 @@ export const PluginToolbarMenu = ({ host, onManage, align = "end", className }:
const groups = getPluginToolbarGroups(snapshot);
const hasActions = groups.some((group) => group.actions.length > 0);
const pluginNames = new Map(snapshot.extensions.map((extension) => [extension.manifest.id, extension.manifest.name]));
const activePanelRegistered = Boolean(activePanelPluginId && activePanelId && snapshot.panels.some(
(panel) => panel.pluginId === activePanelPluginId && panel.id === activePanelId
));
@@ -74,7 +72,7 @@ export const PluginToolbarMenu = ({ host, onManage, align = "end", className }:
}
};
const renderAction = (action: RegisteredPluginAction, prefix: string, pluginName?: string) => {
const renderAction = (action: RegisteredPluginAction, prefix: string) => {
const key = actionKey(action);
return (
<DropdownMenuItem
@@ -94,7 +92,6 @@ export const PluginToolbarMenu = ({ host, onManage, align = "end", className }:
<Play className="h-4 w-4 text-slate-500" />
)}
<span className="min-w-0 flex-1 truncate">{action.title}</span>
{pluginName ? <span className="max-w-24 truncate text-[10px] text-slate-400">{pluginName}</span> : null}
</DropdownMenuItem>
);
};
@@ -121,30 +118,11 @@ export const PluginToolbarMenu = ({ host, onManage, align = "end", className }:
</Tooltip>
</TooltipProvider>
<DropdownMenuContent align={align} className="w-72">
{snapshot.recentActions.length ? (
<>
<DropdownMenuLabel className="flex items-center gap-2 text-xs text-slate-500">
<Clock3 className="h-3.5 w-3.5" />
{t("plugins.toolbar.recent")}
</DropdownMenuLabel>
{snapshot.recentActions.map((action) => renderAction(action, "recent", pluginNames.get(action.pluginId)))}
<DropdownMenuSeparator />
</>
) : null}
{groups.length > 0 ? groups.map((group, index) => (
<div key={group.pluginId}>
{index > 0 ? <DropdownMenuSeparator /> : null}
<DropdownMenuLabel className="truncate text-xs text-slate-500">{group.name}</DropdownMenuLabel>
{group.actions.map((action) => renderAction(action, group.pluginId))}
{group.hasSettings ? (
<DropdownMenuItem asChild className="gap-2" onSelect={() => setOpen(false)}>
<Link to={getPluginDetailPath(group.pluginId, "settings")} aria-label={t("plugins.settings.open", { name: group.name })}>
<Settings2 className="h-4 w-4 text-slate-500" />
{t("plugins.settings.title")}
</Link>
</DropdownMenuItem>
) : null}
</div>
)) : (
<div className="px-2 py-5 text-center text-xs leading-5 text-slate-500">{t("plugins.toolbar.empty")}</div>
@@ -227,7 +227,6 @@ describe("EdgeEverPluginHost", () => {
expect(host.getSnapshot().commands).toHaveLength(7);
expect(host.getSnapshot().panels).toEqual([{ pluginId: "org.edgeever.test-plugin", id: "fixture", title: "Fixture panel", presentation: "dialog" }]);
expect(notices).toEqual(["hello from plugin"]);
expect(host.getSnapshot().recentActions[0]).toMatchObject({ id: "hello", type: "command" });
await host.runCommand("org.edgeever.test-plugin", "read-without-permission");
await host.runCommand("org.edgeever.test-plugin", "subscribe-without-read-permission");
await host.runCommand("org.edgeever.test-plugin", "replace-selection");
@@ -245,7 +244,6 @@ describe("EdgeEverPluginHost", () => {
const container = {};
const disposePanel = await host.mountPanel("org.edgeever.test-plugin", "fixture", container);
expect(container.mountedByFixture).toBe(true);
expect(host.getSnapshot().recentActions[0]).toMatchObject({ id: "fixture", type: "panel" });
disposePanel();
expect(container.mountedByFixture).toBe(false);
const mountedDuringDisable = {};
@@ -253,7 +251,6 @@ describe("EdgeEverPluginHost", () => {
await host.setEnabled("org.edgeever.test-plugin", false);
expect(mountedDuringDisable.mountedByFixture).toBe(false);
expect(host.getSnapshot().panels).toHaveLength(0);
expect(host.getSnapshot().recentActions).toHaveLength(0);
await host.uninstall("org.edgeever.test-plugin");
expect(secrets.has("test:org.edgeever.test-plugin:token")).toBe(false);
expect(secrets.has("test:org.edgeever.test-plugin:setting:token")).toBe(false);
+1 -60
View File
@@ -43,7 +43,6 @@ const INSTALLED_EXTENSIONS_STORAGE_KEY = "edgeever.extensions.installed.v1";
const ACTIVE_THEME_STORAGE_KEY = "edgeever.extensions.active-theme.v1";
const STORAGE_PREFIX = "edgeever.plugin-data.v1";
const SETTINGS_STORAGE_PREFIX = "edgeever.plugin-settings.v1";
const RECENT_ACTIONS_STORAGE_PREFIX = "edgeever.extensions.recent-actions.v1";
const readStorageItem = (key: string) => {
try {
@@ -172,7 +171,6 @@ export interface PluginHostSnapshot {
commands: RegisteredPluginCommand[];
panels: RegisteredPluginPanel[];
embeds: RegisteredPluginEmbed[];
recentActions: RegisteredPluginAction[];
activeThemeId: string | null;
}
@@ -425,8 +423,7 @@ export class EdgeEverPluginHost {
private readonly eventListeners = new Map<keyof PluginEventMap, Set<{ pluginId: string; listener: (payload: never) => void }>>();
private extensions = readInstalledExtensions();
private activeThemeId = readStorageItem(ACTIVE_THEME_STORAGE_KEY);
private snapshot: PluginHostSnapshot = { extensions: [], commands: [], panels: [], embeds: [], recentActions: [], activeThemeId: null };
private recentActions: RegisteredPluginAction[];
private snapshot: PluginHostSnapshot = { extensions: [], commands: [], panels: [], embeds: [], activeThemeId: null };
private editorAdapter: PluginEditorAdapter | null = null;
private navigationAdapter: PluginNavigationAdapter | null = null;
private panelAdapter: PluginPanelAdapter | null = null;
@@ -452,7 +449,6 @@ export class EdgeEverPluginHost {
this.secretStorage = options.secretStorage ?? new WebPluginSecretStore();
this.packageStorage = options.packageStorage ?? new WebPluginPackageStore();
this.scheduleAdapter = options.scheduleAdapter;
this.recentActions = this.readRecentActions();
this.refreshSnapshot();
}
@@ -647,8 +643,6 @@ export class EdgeEverPluginHost {
const extension = this.requireExtension(extensionId);
if (extension.manifest.type === "plugin") await this.deactivatePlugin(extensionId);
this.extensions = this.extensions.filter((item) => item.manifest.id !== extensionId);
this.recentActions = this.recentActions.filter((action) => action.pluginId !== extensionId);
this.persistRecentActions();
if (this.activeThemeId === extensionId) {
this.activeThemeId = null;
removeStorageItem(ACTIVE_THEME_STORAGE_KEY);
@@ -715,7 +709,6 @@ export class EdgeEverPluginHost {
const command = this.commands.get(`${pluginId}:${commandId}`);
if (!command) throw new Error("Plugin command is not registered.");
await command.run();
this.recordRecentAction({ pluginId, id: commandId, title: command.title, type: "command" });
}
async mountPanel(
@@ -749,7 +742,6 @@ export class EdgeEverPluginHost {
if (typeof pluginDispose === "function") pluginDispose();
};
mounted.add(dispose);
this.recordRecentAction({ pluginId, id: panelId, title: panel.title, type: "panel" });
return dispose;
}
@@ -1517,54 +1509,7 @@ export class EdgeEverPluginHost {
this.refreshSnapshot();
}
private readRecentActions(): RegisteredPluginAction[] {
try {
const parsed = JSON.parse(readStorageItem(`${RECENT_ACTIONS_STORAGE_PREFIX}:${this.scope}`) ?? "[]") as unknown;
if (!Array.isArray(parsed)) return [];
return parsed.flatMap((item) => {
if (!item || typeof item !== "object") return [];
const action = item as Partial<RegisteredPluginAction>;
if (
typeof action.pluginId !== "string" ||
typeof action.id !== "string" ||
typeof action.title !== "string" ||
(action.type !== "command" && action.type !== "panel")
) return [];
return [action as RegisteredPluginAction];
}).slice(0, 5);
} catch {
return [];
}
}
private recordRecentAction(action: RegisteredPluginAction) {
this.recentActions = [
action,
...this.recentActions.filter((item) => item.pluginId !== action.pluginId || item.id !== action.id || item.type !== action.type),
].slice(0, 5);
this.persistRecentActions();
this.refreshSnapshot();
}
private persistRecentActions() {
try {
writeStorageItem(`${RECENT_ACTIONS_STORAGE_PREFIX}:${this.scope}`, JSON.stringify(this.recentActions));
} catch {
// Recent actions are a convenience and must never make a successful plugin action fail.
}
}
private refreshSnapshot() {
const registeredActions = new Map<string, RegisteredPluginAction>([
...[...this.commands.values()].map(({ pluginId, id, title }) => [
`command:${pluginId}:${id}`,
{ pluginId, id, title, type: "command" as const },
] as const),
...[...this.panels.values()].map(({ pluginId, id, title }) => [
`panel:${pluginId}:${id}`,
{ pluginId, id, title, type: "panel" as const },
] as const),
]);
this.snapshot = {
extensions: this.extensions.map((item) => ({ ...item, manifest: { ...item.manifest } })),
commands: [...this.commands.values()].map(({ pluginId, id, title }) => ({ pluginId, id, title })),
@@ -1575,10 +1520,6 @@ export class EdgeEverPluginHost {
presentation: presentation === "fullscreen" ? "fullscreen" : "dialog",
})),
embeds: [...this.embeds.values()].map(({ pluginId, type }) => ({ pluginId, type })),
recentActions: this.recentActions.flatMap((action) => {
const registered = registeredActions.get(`${action.type}:${action.pluginId}:${action.id}`);
return registered ? [registered] : [];
}),
activeThemeId: this.activeThemeId,
};
for (const listener of this.listeners) listener();
@@ -17,18 +17,16 @@ describe("plugin settings navigation", () => {
expect(hasPluginSettings({ type: "theme" })).toBe(false);
});
test("disabled plugins retain settings but never expose executable actions", () => {
test("does not expose disabled plugins through the command menu", () => {
const groups = getPluginToolbarGroups({
extensions: [{ manifest: plugin, enabled: false }],
commands: [{ pluginId: "example", id: "run" }],
panels: [{ pluginId: "example", id: "panel" }],
});
expect(groups).toHaveLength(1);
expect(groups[0].hasSettings).toBe(true);
expect(groups[0].actions).toEqual([]);
expect(groups).toEqual([]);
});
test("keeps commands and panels while hiding settings-free inactive plugins and themes", () => {
test("keeps commands and panels while hiding settings-only plugins, inactive plugins, and themes", () => {
const groups = getPluginToolbarGroups({
extensions: [
{ manifest: { type: "plugin", id: "actions" }, enabled: true },
@@ -39,11 +37,8 @@ describe("plugin settings navigation", () => {
commands: [{ pluginId: "actions", id: "run" }, { pluginId: "inactive", id: "hidden" }],
panels: [{ pluginId: "actions", id: "panel" }],
});
expect(groups.map((group) => group.pluginId)).toEqual(["actions", "example"]);
expect(groups[0].hasSettings).toBe(false);
expect(groups.map((group) => group.pluginId)).toEqual(["actions"]);
expect(groups[0].actions.map((action) => action.type)).toEqual(["command", "panel"]);
expect(groups[1].hasSettings).toBe(true);
expect(groups[1].actions).toEqual([]);
});
test("resolves deep links and safely falls back for unsupported settings pages", () => {
@@ -15,12 +15,11 @@ export const getPluginDetailPage = (manifest: ExtensionManifest, requestedPage:
export const getPluginToolbarGroups = (snapshot: Pick<PluginHostSnapshot, "extensions" | "commands" | "panels">) =>
snapshot.extensions.flatMap((extension) => {
if (extension.manifest.type !== "plugin") return [];
const hasSettings = hasPluginSettings(extension.manifest);
const actions: RegisteredPluginAction[] = extension.enabled ? [
...snapshot.commands.filter((command) => command.pluginId === extension.manifest.id).map((command) => ({ ...command, type: "command" as const })),
...snapshot.panels.filter((panel) => panel.pluginId === extension.manifest.id).map((panel) => ({ ...panel, type: "panel" as const })),
] : [];
return actions.length || hasSettings
? [{ pluginId: extension.manifest.id, name: extension.manifest.name, actions, hasSettings }]
return actions.length
? [{ pluginId: extension.manifest.id, name: extension.manifest.name, actions }]
: [];
});
-1
View File
@@ -759,7 +759,6 @@ export const enUS = {
},
toolbar: {
open: "Plugins",
recent: "Recently used",
empty: "Enabled plugins have not provided any commands or panels yet.",
manage: "Manage plugins and themes",
},
-1
View File
@@ -759,7 +759,6 @@ export const zhCN = {
},
toolbar: {
open: "插件",
recent: "最近使用",
empty: "已启用的插件还没有提供命令或面板。",
manage: "管理插件与主题",
},