fix(plugins): stop listing every command and panel on cards and menus
Marketplace cards keep launcher commands only. The toolbar menu keeps editor commands and dashboard panels, and drops workflow dialogs plus commands that only open a dashboard already in the menu.
This commit is contained in:
1 parent
2e9b63fe9c
commit
9238e81e6a
10 files changed
+177
-56
No files matched your search
@@ -1,4 +1,4 @@
|
||||
import { BadgeCheck, Download, PanelRightOpen, Play, Settings2, Trash2 } from "lucide-react";
|
||||
import { BadgeCheck, Download, Play, Settings2, Trash2 } from "lucide-react";
|
||||
import { Link } from "react-router";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -13,9 +13,9 @@ import {
|
||||
getPluginCatalogVersion,
|
||||
type PluginCatalogItem,
|
||||
} from "@/lib/plugins/plugin-catalog";
|
||||
import { getPluginDetailPath, hasPluginSettings } from "@/lib/plugins/plugin-navigation";
|
||||
import { getPluginDetailPath, hasPluginSettings, isPluginCardCommand } from "@/lib/plugins/plugin-navigation";
|
||||
import type { PluginUpdateInfo } from "@/lib/plugins/plugin-updates";
|
||||
import type { RegisteredPluginCommand, RegisteredPluginPanel } from "@/lib/plugins/plugin-host";
|
||||
import type { RegisteredPluginCommand } from "@/lib/plugins/plugin-host";
|
||||
|
||||
const permissionLabel = (permission: string) => permission.replace(":", " · ");
|
||||
|
||||
@@ -28,27 +28,23 @@ export const PluginCatalogCard = ({
|
||||
item,
|
||||
update,
|
||||
commands,
|
||||
panels,
|
||||
pendingId,
|
||||
onOpenPlugin,
|
||||
onToggle,
|
||||
onInstallMarketplace,
|
||||
onUpdate,
|
||||
onRunCommand,
|
||||
onOpenPanel,
|
||||
onUninstall,
|
||||
}: {
|
||||
item: PluginCatalogItem;
|
||||
update?: PluginUpdateInfo;
|
||||
commands: RegisteredPluginCommand[];
|
||||
panels: RegisteredPluginPanel[];
|
||||
pendingId: string | null;
|
||||
onOpenPlugin?: (pluginId: string) => void;
|
||||
onToggle: (enabled: boolean) => void;
|
||||
onInstallMarketplace: () => void;
|
||||
onUpdate: () => void;
|
||||
onRunCommand: (command: RegisteredPluginCommand) => void;
|
||||
onOpenPanel: (panel: RegisteredPluginPanel) => void;
|
||||
onUninstall: () => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
@@ -195,7 +191,7 @@ export const PluginCatalogCard = ({
|
||||
{pendingId === marketplaceActionId ? t("plugins.installing") : t("plugins.marketplace.installVerified")}
|
||||
</Button>
|
||||
) : null}
|
||||
{extension ? commands.map((command) => (
|
||||
{extension ? commands.filter(isPluginCardCommand).map((command) => (
|
||||
<Button
|
||||
key={command.id}
|
||||
size="sm"
|
||||
@@ -208,18 +204,6 @@ export const PluginCatalogCard = ({
|
||||
{command.title}
|
||||
</Button>
|
||||
)) : null}
|
||||
{extension ? panels.map((panel) => (
|
||||
<Button
|
||||
key={panel.id}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-8 gap-1.5 text-xs"
|
||||
onClick={() => onOpenPanel(panel)}
|
||||
>
|
||||
<PanelRightOpen className="h-3.5 w-3.5" />
|
||||
{panel.title}
|
||||
</Button>
|
||||
)) : null}
|
||||
{extension ? (
|
||||
<Button
|
||||
size="sm"
|
||||
|
||||
@@ -13,6 +13,12 @@ describe("plugin manager card layout", () => {
|
||||
expect(source).not.toContain("md:grid-cols-2");
|
||||
});
|
||||
|
||||
test("does not dump every plugin panel onto marketplace cards", () => {
|
||||
expect(catalogCard).not.toContain("onOpenPanel");
|
||||
expect(catalogCard).toContain("isPluginCardCommand");
|
||||
expect(catalogCard).toContain("commands.filter(isPluginCardCommand)");
|
||||
});
|
||||
|
||||
test("uses GitHub icon links for marketplace and installed extension repositories", () => {
|
||||
expect(catalogCard.match(/<GitHubRepositoryLink/g)).toHaveLength(1);
|
||||
expect(catalogCard).toContain("showTooltip={false}");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState, useSyncExternalStore } from "react";
|
||||
import { BookOpen, CalendarClock, Download, ExternalLink, History, PanelRightOpen, Play, Puzzle, RefreshCw, Settings2, Trash2 } from "lucide-react";
|
||||
import { BookOpen, CalendarClock, Download, ExternalLink, History, Play, Puzzle, RefreshCw, Settings2, Trash2 } from "lucide-react";
|
||||
import { Link } from "react-router";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
@@ -7,16 +7,15 @@ import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import type { EdgeEverPluginHost, InstalledExtension, RegisteredPluginCommand, RegisteredPluginPanel } from "@/lib/plugins/plugin-host";
|
||||
import type { EdgeEverPluginHost, InstalledExtension, RegisteredPluginCommand } from "@/lib/plugins/plugin-host";
|
||||
import { PluginCatalogCard } from "@/components/plugins/PluginCatalogCard";
|
||||
import { PluginPanelDialog } from "@/components/plugins/PluginPanelDialog";
|
||||
import { loadResolvedPluginMarketplace } from "@/lib/plugins/plugin-marketplace";
|
||||
import { GitHubMark } from "@/components/GitHubRepositoryLink";
|
||||
import { applyPluginUpdate, checkPluginUpdates, type PluginUpdateInfo } from "@/lib/plugins/plugin-updates";
|
||||
import { PluginUpdateDialog } from "@/components/plugins/PluginUpdateDialog";
|
||||
import { PluginSettingsSection } from "@/components/plugins/PluginSettingsSection";
|
||||
import { buildPluginCatalogItems, getPluginCatalogSourceKey } from "@/lib/plugins/plugin-catalog";
|
||||
import { getPluginDetailPage, getPluginDetailPath, hasPluginSettings, type PluginDetailPage } from "@/lib/plugins/plugin-navigation";
|
||||
import { getPluginDetailPage, getPluginDetailPath, hasPluginSettings, isPluginCardCommand, type PluginDetailPage } from "@/lib/plugins/plugin-navigation";
|
||||
import type { ScheduledTask } from "@edgeever/shared";
|
||||
import { api, getOrCreateClientDeviceId } from "@/lib/api";
|
||||
import { ScheduledTaskRunHistoryDialog } from "@/components/execution/ScheduledTaskRunHistoryDialog";
|
||||
@@ -140,10 +139,8 @@ const PluginDetailView = ({
|
||||
commands,
|
||||
extension,
|
||||
host,
|
||||
panels,
|
||||
pendingId,
|
||||
update,
|
||||
onOpenPanel,
|
||||
onRunCommand,
|
||||
onToggle,
|
||||
onUninstall,
|
||||
@@ -153,10 +150,8 @@ const PluginDetailView = ({
|
||||
commands: RegisteredPluginCommand[];
|
||||
extension: InstalledExtension;
|
||||
host: EdgeEverPluginHost;
|
||||
panels: RegisteredPluginPanel[];
|
||||
pendingId: string | null;
|
||||
update?: PluginUpdateInfo;
|
||||
onOpenPanel: (panel: RegisteredPluginPanel) => void;
|
||||
onRunCommand: (command: RegisteredPluginCommand) => void;
|
||||
onToggle: (enabled: boolean) => void;
|
||||
onUninstall: () => void;
|
||||
@@ -261,18 +256,12 @@ const PluginDetailView = ({
|
||||
{t("plugins.updates.update")}
|
||||
</Button>
|
||||
) : null}
|
||||
{commands.map((command) => (
|
||||
{commands.filter(isPluginCardCommand).map((command) => (
|
||||
<Button key={command.id} size="sm" variant="outline" className="gap-1.5" disabled={pendingId === `${id}:${command.id}`} onClick={() => onRunCommand(command)}>
|
||||
<Play className="h-3.5 w-3.5" />
|
||||
{command.title}
|
||||
</Button>
|
||||
))}
|
||||
{panels.map((panel) => (
|
||||
<Button key={panel.id} size="sm" variant="outline" className="gap-1.5" onClick={() => onOpenPanel(panel)}>
|
||||
<PanelRightOpen className="h-3.5 w-3.5" />
|
||||
{panel.title}
|
||||
</Button>
|
||||
))}
|
||||
<Button size="sm" variant="ghost" className="ml-auto gap-1.5 text-rose-600 hover:bg-rose-50 hover:text-rose-700" disabled={pendingId === `remove:${id}`} onClick={onUninstall}>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
{t("plugins.uninstall")}
|
||||
@@ -309,7 +298,6 @@ export const PluginManagerCard = ({
|
||||
const [manuallyChecking, setManuallyChecking] = useState(false);
|
||||
const [lastManualCheckCount, setLastManualCheckCount] = useState<number | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [activePanel, setActivePanel] = useState<RegisteredPluginPanel | null>(null);
|
||||
const [pendingUpdate, setPendingUpdate] = useState<PluginUpdateInfo | null>(null);
|
||||
const [pendingTrustPluginId, setPendingTrustPluginId] = useState<string | null>(null);
|
||||
const marketplaceQuery = useQuery({ queryKey: ["plugin-marketplace", "v1"], queryFn: () => loadResolvedPluginMarketplace(), staleTime: 5 * 60_000 });
|
||||
@@ -330,15 +318,6 @@ export const PluginManagerCard = ({
|
||||
const selectedExtension = selectedPluginId
|
||||
? snapshot.extensions.find((extension) => extension.manifest.id === selectedPluginId)
|
||||
: undefined;
|
||||
const activePanelPluginId = activePanel?.pluginId ?? null;
|
||||
const activePanelId = activePanel?.id ?? null;
|
||||
const activePanelRegistered = Boolean(activePanelPluginId && activePanelId && snapshot.panels.some(
|
||||
(panel) => panel.pluginId === activePanelPluginId && panel.id === activePanelId
|
||||
));
|
||||
|
||||
useEffect(() => {
|
||||
if (activePanelId && activePanelPluginId && !activePanelRegistered) setActivePanel(null);
|
||||
}, [activePanelId, activePanelPluginId, activePanelRegistered]);
|
||||
|
||||
const install = async () => {
|
||||
if (!manifestUrl.trim()) return;
|
||||
@@ -489,7 +468,6 @@ export const PluginManagerCard = ({
|
||||
host={host}
|
||||
update={updateQuery.data?.updates.find((update) => update.pluginId === selectedExtension.manifest.id)}
|
||||
commands={snapshot.commands.filter((command) => command.pluginId === selectedExtension.manifest.id)}
|
||||
panels={snapshot.panels.filter((panel) => panel.pluginId === selectedExtension.manifest.id)}
|
||||
pendingId={pendingId}
|
||||
onToggle={(enabled) => toggleExtension(selectedExtension, enabled)}
|
||||
onUpdate={() => {
|
||||
@@ -500,7 +478,6 @@ export const PluginManagerCard = ({
|
||||
`${selectedExtension.manifest.id}:${command.id}`,
|
||||
() => host.runCommand(selectedExtension.manifest.id, command.id),
|
||||
)}
|
||||
onOpenPanel={setActivePanel}
|
||||
onUninstall={() => void run(`remove:${selectedExtension.manifest.id}`, async () => {
|
||||
await host.uninstall(selectedExtension.manifest.id);
|
||||
onClosePlugin?.();
|
||||
@@ -538,7 +515,6 @@ export const PluginManagerCard = ({
|
||||
item={item}
|
||||
update={updateQuery.data?.updates.find((update) => update.pluginId === item.id)}
|
||||
commands={snapshot.commands.filter((command) => command.pluginId === item.id)}
|
||||
panels={snapshot.panels.filter((panel) => panel.pluginId === item.id)}
|
||||
pendingId={pendingId}
|
||||
onOpenPlugin={onOpenPlugin}
|
||||
onToggle={(enabled) => {
|
||||
@@ -559,7 +535,6 @@ export const PluginManagerCard = ({
|
||||
`${item.id}:${command.id}`,
|
||||
() => host.runCommand(item.id, command.id),
|
||||
)}
|
||||
onOpenPanel={setActivePanel}
|
||||
onUninstall={() => void run(`remove:${item.id}`, () => host.uninstall(item.id))}
|
||||
/>
|
||||
))}
|
||||
@@ -568,7 +543,6 @@ export const PluginManagerCard = ({
|
||||
</>
|
||||
)}
|
||||
|
||||
<PluginPanelDialog host={host} panel={activePanel} onClose={() => setActivePanel(null)} />
|
||||
{pendingUpdate ? (
|
||||
<PluginUpdateDialog
|
||||
update={pendingUpdate}
|
||||
|
||||
@@ -124,12 +124,15 @@ export interface RegisteredPluginCommand {
|
||||
pluginId: string;
|
||||
id: string;
|
||||
title: string;
|
||||
listed?: boolean;
|
||||
menu?: boolean;
|
||||
}
|
||||
|
||||
export interface RegisteredPluginPanel {
|
||||
pluginId: string;
|
||||
id: string;
|
||||
title: string;
|
||||
purpose?: "workflow" | "dashboard" | "preview" | "onboarding";
|
||||
presentation: "dialog" | "fullscreen";
|
||||
}
|
||||
|
||||
@@ -1529,11 +1532,18 @@ export class EdgeEverPluginHost {
|
||||
private refreshSnapshot() {
|
||||
this.snapshot = {
|
||||
extensions: this.extensions.map((item) => ({ ...item, manifest: { ...item.manifest } })),
|
||||
commands: [...this.commands.values()].map(({ pluginId, id, title }) => ({ pluginId, id, title })),
|
||||
panels: [...this.panels.values()].map(({ pluginId, id, title, presentation }) => ({
|
||||
commands: [...this.commands.values()].map(({ pluginId, id, title, listed, menu }) => ({
|
||||
pluginId,
|
||||
id,
|
||||
title,
|
||||
listed: listed === false ? false : undefined,
|
||||
menu: menu === false ? false : undefined,
|
||||
})),
|
||||
panels: [...this.panels.values()].map(({ pluginId, id, title, purpose, presentation }) => ({
|
||||
pluginId,
|
||||
id,
|
||||
title,
|
||||
purpose,
|
||||
presentation: presentation === "fullscreen" ? "fullscreen" : "dialog",
|
||||
})),
|
||||
embeds: [...this.embeds.values()].map(({ pluginId, type }) => ({ pluginId, type })),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { getPluginDetailPage, getPluginDetailPath, getPluginToolbarGroups, hasPluginSettings } from "./plugin-navigation.ts";
|
||||
import { getPluginDetailPage, getPluginDetailPath, getPluginToolbarGroups, hasPluginSettings, isPluginCardCommand, isPluginMenuCommand, isPluginMenuPanel } from "./plugin-navigation.ts";
|
||||
|
||||
const plugin = { type: "plugin", id: "example", settings: { fields: [{ key: "token", type: "secret", label: "Token" }] } };
|
||||
|
||||
@@ -17,6 +17,51 @@ describe("plugin settings navigation", () => {
|
||||
expect(hasPluginSettings({ type: "theme" })).toBe(false);
|
||||
});
|
||||
|
||||
test("omits unlisted commands from marketplace cards but keeps them in the toolbar menu", () => {
|
||||
expect(isPluginCardCommand({ listed: undefined })).toBe(true);
|
||||
expect(isPluginCardCommand({ listed: true })).toBe(true);
|
||||
expect(isPluginCardCommand({ listed: false })).toBe(false);
|
||||
expect(isPluginMenuCommand({ menu: undefined })).toBe(true);
|
||||
expect(isPluginMenuCommand({ menu: false })).toBe(false);
|
||||
expect(isPluginMenuPanel({ purpose: "dashboard" })).toBe(true);
|
||||
expect(isPluginMenuPanel({ purpose: "workflow" })).toBe(false);
|
||||
const groups = getPluginToolbarGroups({
|
||||
extensions: [{ manifest: { type: "plugin", id: "actions" }, enabled: true }],
|
||||
commands: [{ pluginId: "actions", id: "insert", listed: false }, { pluginId: "actions", id: "open" }],
|
||||
panels: [{ pluginId: "actions", id: "panel", purpose: "dashboard" }],
|
||||
});
|
||||
expect(groups[0].actions.map((action) => action.id)).toEqual(["insert", "open", "panel"]);
|
||||
});
|
||||
|
||||
test("keeps editor commands in the toolbar and drops duplicate dashboard launchers", () => {
|
||||
const groups = getPluginToolbarGroups({
|
||||
extensions: [{ manifest: { type: "plugin", id: "tasks" }, enabled: true }],
|
||||
commands: [
|
||||
{ pluginId: "tasks", id: "open-dashboard", title: "打开待办任务面板", menu: false },
|
||||
{ pluginId: "tasks", id: "insert-task", title: "在光标处插入待办任务", listed: false },
|
||||
{ pluginId: "tasks", id: "create-or-edit", title: "创建或编辑任务", listed: false },
|
||||
],
|
||||
panels: [
|
||||
{ pluginId: "tasks", id: "tasks", title: "待办任务", purpose: "dashboard" },
|
||||
{ pluginId: "tasks", id: "edit-task", title: "创建或编辑任务", purpose: "workflow" },
|
||||
],
|
||||
});
|
||||
expect(groups[0].actions.map((action) => `${action.type}:${action.id}`)).toEqual([
|
||||
"command:insert-task",
|
||||
"command:create-or-edit",
|
||||
"panel:tasks",
|
||||
]);
|
||||
});
|
||||
|
||||
test("hides a dashboard opener command even when menu is left at the default", () => {
|
||||
const groups = getPluginToolbarGroups({
|
||||
extensions: [{ manifest: { type: "plugin", id: "tasks" }, enabled: true }],
|
||||
commands: [{ pluginId: "tasks", id: "open-dashboard", title: "打开待办任务面板" }],
|
||||
panels: [{ pluginId: "tasks", id: "tasks", title: "待办任务", purpose: "dashboard" }],
|
||||
});
|
||||
expect(groups[0].actions.map((action) => `${action.type}:${action.id}`)).toEqual(["panel:tasks"]);
|
||||
});
|
||||
|
||||
test("does not expose disabled plugins through the command menu", () => {
|
||||
const groups = getPluginToolbarGroups({
|
||||
extensions: [{ manifest: plugin, enabled: false }],
|
||||
@@ -35,7 +80,7 @@ describe("plugin settings navigation", () => {
|
||||
{ manifest: { type: "theme", id: "theme" }, enabled: true },
|
||||
],
|
||||
commands: [{ pluginId: "actions", id: "run" }, { pluginId: "inactive", id: "hidden" }],
|
||||
panels: [{ pluginId: "actions", id: "panel" }],
|
||||
panels: [{ pluginId: "actions", id: "panel", purpose: "dashboard" }],
|
||||
});
|
||||
expect(groups.map((group) => group.pluginId)).toEqual(["actions"]);
|
||||
expect(groups[0].actions.map((action) => action.type)).toEqual(["command", "panel"]);
|
||||
|
||||
@@ -12,12 +12,40 @@ export const getPluginDetailPath = (pluginId: string, page: PluginDetailPage = "
|
||||
export const getPluginDetailPage = (manifest: ExtensionManifest, requestedPage: string | null): PluginDetailPage =>
|
||||
requestedPage === "settings" && hasPluginSettings(manifest) ? "settings" : "overview";
|
||||
|
||||
/** Marketplace and manager cards list launcher commands; editor-context commands stay in the toolbar menu. */
|
||||
export const isPluginCardCommand = (command: { listed?: boolean }) => command.listed !== false;
|
||||
|
||||
export const isPluginMenuCommand = (command: { menu?: boolean }) => command.menu !== false;
|
||||
|
||||
export const isPluginMenuPanel = (panel: { purpose?: string }) =>
|
||||
panel.purpose === "dashboard" || panel.purpose === "onboarding" || !panel.purpose;
|
||||
|
||||
const isDashboardLauncherCommand = (
|
||||
command: { title: string },
|
||||
panels: Array<{ title: string; purpose?: string }>,
|
||||
) => panels.some((panel) => {
|
||||
if (panel.purpose && panel.purpose !== "dashboard" && panel.purpose !== "onboarding") return false;
|
||||
const panelTitle = panel.title?.trim() ?? "";
|
||||
const title = command.title?.trim() ?? "";
|
||||
if (!panelTitle || !title) return false;
|
||||
if (title === `打开${panelTitle}` || title === `打开${panelTitle}面板`) return true;
|
||||
const lower = title.toLocaleLowerCase();
|
||||
const panelLower = panelTitle.toLocaleLowerCase();
|
||||
return lower === `open ${panelLower}` || lower === `open ${panelLower} dashboard`;
|
||||
});
|
||||
|
||||
export const getPluginToolbarGroups = (snapshot: Pick<PluginHostSnapshot, "extensions" | "commands" | "panels">) =>
|
||||
snapshot.extensions.flatMap((extension) => {
|
||||
if (extension.manifest.type !== "plugin") return [];
|
||||
const pluginPanels = snapshot.panels.filter((panel) => panel.pluginId === extension.manifest.id && isPluginMenuPanel(panel));
|
||||
const pluginCommands = snapshot.commands.filter((command) => (
|
||||
command.pluginId === extension.manifest.id
|
||||
&& isPluginMenuCommand(command)
|
||||
&& !isDashboardLauncherCommand(command, pluginPanels)
|
||||
));
|
||||
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 })),
|
||||
...pluginCommands.map((command) => ({ ...command, type: "command" as const })),
|
||||
...pluginPanels.map((panel) => ({ ...panel, type: "panel" as const })),
|
||||
] : [];
|
||||
return actions.length
|
||||
? [{ pluginId: extension.manifest.id, name: extension.manifest.name, actions }]
|
||||
|
||||
@@ -153,6 +153,32 @@ export default definePlugin({
|
||||
|
||||
Every registration returns a disposer. The host also disposes registered commands and events automatically when a plugin is disabled.
|
||||
|
||||
Commands appear on the plugin marketplace card by default. Set `listed: false` for editor-context or secondary commands that belong in the plugin toolbar menu instead of the install card:
|
||||
|
||||
```js
|
||||
context.commands.register({
|
||||
id: "insert-task",
|
||||
title: "Insert task at cursor",
|
||||
listed: false,
|
||||
async run() {
|
||||
await context.editor.insertAtCursor("- [ ] ");
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
Workflow and preview panels are also omitted from that card. Open them from a command, the toolbar menu, or another panel.
|
||||
|
||||
The plugin toolbar menu lists editor commands and dashboard or onboarding panels. It omits workflow or preview dialogs, and omits a command that only opens a dashboard already in the menu. Set `menu: false` when a command should appear on the marketplace card but not next to its dashboard panel:
|
||||
|
||||
```js
|
||||
context.commands.register({
|
||||
id: "open-dashboard",
|
||||
title: "Open task dashboard",
|
||||
menu: false,
|
||||
run: () => context.ui.panels.open("tasks"),
|
||||
});
|
||||
```
|
||||
|
||||
## Schedules API
|
||||
|
||||
Desktop plugins can persistently schedule one of their own registered commands. Register the command first, then use a stable plugin-local key with `upsert()`:
|
||||
|
||||
@@ -153,6 +153,32 @@ export default definePlugin({
|
||||
|
||||
每次注册都会返回清理函数。插件停用时,宿主也会自动清理已注册的命令和事件。
|
||||
|
||||
命令默认会出现在插件市场卡片上。编辑器上下文或次要命令应设 `listed: false`,只保留在插件工具栏菜单中,不要放在安装卡片上:
|
||||
|
||||
```js
|
||||
context.commands.register({
|
||||
id: "insert-task",
|
||||
title: "在光标处插入待办任务",
|
||||
listed: false,
|
||||
async run() {
|
||||
await context.editor.insertAtCursor("- [ ] ");
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
工作流和预览面板也不会出现在该卡片上。请从命令、工具栏菜单或另一个面板打开它们。
|
||||
|
||||
插件工具栏菜单列出编辑器命令以及 dashboard / onboarding 面板。工作流或预览对话框不会出现在菜单里;如果某条命令只是打开菜单中已有的 dashboard,也会被省略。卡片上的启动命令如果和 dashboard 面板重复,应设 `menu: false`:
|
||||
|
||||
```js
|
||||
context.commands.register({
|
||||
id: "open-dashboard",
|
||||
title: "打开待办任务面板",
|
||||
menu: false,
|
||||
run: () => context.ui.panels.open("tasks"),
|
||||
});
|
||||
```
|
||||
|
||||
## 定时任务 API
|
||||
|
||||
桌面插件可以持久化定时执行自己的已注册命令。先注册命令,再用稳定的插件内计划键调用 `upsert()`:
|
||||
|
||||
Vendored
+11
@@ -268,6 +268,17 @@ export type PluginEventMap = {
|
||||
export interface PluginCommand {
|
||||
id: string;
|
||||
title: string;
|
||||
/**
|
||||
* When false, the command stays in the plugin toolbar menu but is omitted from
|
||||
* marketplace and plugin-manager cards. Defaults to true.
|
||||
*/
|
||||
listed?: boolean;
|
||||
/**
|
||||
* When false, the command is omitted from the plugin toolbar menu.
|
||||
* Use this for a card-only launcher that already has a dashboard panel in the menu.
|
||||
* Defaults to true.
|
||||
*/
|
||||
menu?: boolean;
|
||||
run: () => void | Promise<void>;
|
||||
}
|
||||
export type PluginScheduleMissedRunPolicy = "run-once" | "skip";
|
||||
|
||||
@@ -294,6 +294,17 @@ export type PluginEventMap = {
|
||||
export interface PluginCommand {
|
||||
id: string;
|
||||
title: string;
|
||||
/**
|
||||
* When false, the command stays in the plugin toolbar menu but is omitted from
|
||||
* marketplace and plugin-manager cards. Defaults to true.
|
||||
*/
|
||||
listed?: boolean;
|
||||
/**
|
||||
* When false, the command is omitted from the plugin toolbar menu.
|
||||
* Use this for a card-only launcher that already has a dashboard panel in the menu.
|
||||
* Defaults to true.
|
||||
*/
|
||||
menu?: boolean;
|
||||
run: () => void | Promise<void>;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user