FN-7454: show Linear Import in Plugin Manager
Register Linear Import as a built-in plugin so users can install and manage it from Plugin Manager. - Add fusion-plugin-linear-import to the built-in plugin catalog. - Extend Plugin Manager registry and rendering coverage for the Linear Import entry. - Add a patch changeset documenting the operator-visible plugin visibility fix. Files changed: .changeset/fn-7454-linear-plugin-visibility.md | 7 ++ .../dashboard/app/components/PluginManager.tsx | 11 +++ .../__tests__/PluginManager.registry.test.tsx | 72 ++++++++++++++++++- .../components/__tests__/PluginManager.test.tsx | 80 ++++++++++++++++++++++ 4 files changed, 168 insertions(+), 2 deletions(-) Fusion-Task-Id: FN-7454 Fusion-Task-Lineage: 1b54adb4-78cc-47cc-ad7a-d675de64fd49 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-7454-linear-plugin-visibility.md
Normal file
7
.changeset/fn-7454-linear-plugin-visibility.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Show the bundled Linear Import plugin in Plugin Manager and dashboard plugin surfaces.
|
||||
category: fix
|
||||
dev: Keeps fusion-plugin-linear-import registered across the built-in Plugin Manager catalog while reusing existing registry, dashboard view, and bundled packaging paths.
|
||||
@@ -176,6 +176,17 @@ export const BUILTIN_PLUGINS: BuiltinPlugin[] = [
|
||||
category: "integration",
|
||||
path: "./plugins/fusion-plugin-compound-engineering",
|
||||
},
|
||||
/*
|
||||
* FNXC:PluginManager 2026-07-02-17:56:
|
||||
* FN-7454 keeps Linear Import in the built-in catalog because FN-7443 shipped the plugin package, registry entry, and dashboard view, but users still could not install or manage it from Plugin Manager without this bundled-plugin registration.
|
||||
*/
|
||||
{
|
||||
id: "fusion-plugin-linear-import",
|
||||
name: "Linear Import",
|
||||
description: "Browse Linear issues and import selected issues as Fusion triage tasks through plugin-owned settings.",
|
||||
category: "integration",
|
||||
path: "./plugins/fusion-plugin-linear-import",
|
||||
},
|
||||
{
|
||||
id: BUILTIN_AGENT_BROWSER_PLUGIN_ID,
|
||||
name: "Agent Browser",
|
||||
|
||||
@@ -97,8 +97,8 @@ function stubEventSource() {
|
||||
vi.stubGlobal("EventSource", MockEventSource);
|
||||
}
|
||||
|
||||
async function renderRegistry(entries: RegistryPluginEntry[] = registryEntries) {
|
||||
vi.mocked(fetchPlugins).mockResolvedValue([installedPlugin]);
|
||||
async function renderRegistry(entries: RegistryPluginEntry[] = registryEntries, installed: PluginInstallation[] = [installedPlugin]) {
|
||||
vi.mocked(fetchPlugins).mockResolvedValue(installed);
|
||||
vi.mocked(fetchPluginRegistry).mockResolvedValue(entries);
|
||||
render(<PluginManager addToast={addToast} />);
|
||||
await act(async () => {
|
||||
@@ -151,6 +151,74 @@ describe("PluginManager registry browsing", () => {
|
||||
expect(within(comingSoon).getByText("Coming Soon")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
state: "not-installed" as const,
|
||||
entry: {
|
||||
id: "fusion-plugin-linear-import",
|
||||
name: "Linear Import",
|
||||
description: "Browse Linear issues and import selected issues as Fusion triage tasks through plugin-owned settings.",
|
||||
version: "0.1.0",
|
||||
author: "Fusion",
|
||||
category: "integration" as const,
|
||||
path: "./plugins/fusion-plugin-linear-import",
|
||||
tags: ["linear", "import", "issues", "dashboard"],
|
||||
installed: false,
|
||||
canInstall: true,
|
||||
},
|
||||
installed: [] as PluginInstallation[],
|
||||
action: "Install",
|
||||
},
|
||||
{
|
||||
state: "installed-error" as const,
|
||||
entry: {
|
||||
id: "fusion-plugin-linear-import",
|
||||
name: "Linear Import",
|
||||
description: "Browse Linear issues and import selected issues as Fusion triage tasks through plugin-owned settings.",
|
||||
version: "0.1.0",
|
||||
author: "Fusion",
|
||||
category: "integration" as const,
|
||||
path: "./plugins/fusion-plugin-linear-import",
|
||||
tags: ["linear", "import", "issues", "dashboard"],
|
||||
installed: true,
|
||||
installedVersion: "0.1.0",
|
||||
state: "error" as const,
|
||||
canInstall: true,
|
||||
},
|
||||
installed: [{ ...installedPlugin, id: "fusion-plugin-linear-import", name: "Linear Import", state: "error" as const, enabled: true, error: "Linear plugin failed" }],
|
||||
action: "Manage",
|
||||
},
|
||||
{
|
||||
state: "installed-started" as const,
|
||||
entry: {
|
||||
id: "fusion-plugin-linear-import",
|
||||
name: "Linear Import",
|
||||
description: "Browse Linear issues and import selected issues as Fusion triage tasks through plugin-owned settings.",
|
||||
version: "0.1.0",
|
||||
author: "Fusion",
|
||||
category: "integration" as const,
|
||||
path: "./plugins/fusion-plugin-linear-import",
|
||||
tags: ["linear", "import", "issues", "dashboard"],
|
||||
installed: true,
|
||||
installedVersion: "0.1.0",
|
||||
state: "started" as const,
|
||||
canInstall: true,
|
||||
},
|
||||
installed: [{ ...installedPlugin, id: "fusion-plugin-linear-import", name: "Linear Import", state: "started" as const, enabled: true }],
|
||||
action: "Manage",
|
||||
},
|
||||
])("shows Linear Import registry action for $state", async ({ entry, installed, action }) => {
|
||||
await renderRegistry([entry], installed);
|
||||
|
||||
const section = screen.getByRole("region", { name: "Browse Registry" });
|
||||
const linear = within(section).getByText("Linear Import").closest(".plugin-registry-item") as HTMLElement;
|
||||
expect(linear).toBeInTheDocument();
|
||||
expect(within(linear).getByText("Browse Linear issues and import selected issues as Fusion triage tasks through plugin-owned settings.")).toBeInTheDocument();
|
||||
expect(within(linear).getByText("integration")).toBeInTheDocument();
|
||||
expect(within(linear).getByRole("button", { name: action })).toBeInTheDocument();
|
||||
expect(within(section).getAllByText("Linear Import")).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("installs registry plugins with their manifest path and refreshes installed plugins", async () => {
|
||||
await renderRegistry();
|
||||
expect(fetchPlugins).toHaveBeenCalledTimes(1);
|
||||
|
||||
@@ -113,6 +113,7 @@ vi.mock("../../hooks/useConfirm", () => ({
|
||||
import {
|
||||
AGENT_BROWSER_SETTINGS_SCHEMA,
|
||||
BUILTIN_AGENT_BROWSER_PLUGIN_ID,
|
||||
BUILTIN_PLUGINS,
|
||||
PluginManager,
|
||||
STATE_COLORS,
|
||||
} from "../PluginManager";
|
||||
@@ -131,6 +132,15 @@ import {
|
||||
} from "../../api";
|
||||
|
||||
const addToast = vi.fn();
|
||||
const LINEAR_PLUGIN_ID = "fusion-plugin-linear-import";
|
||||
|
||||
function getBuiltInPluginCard(name: string): HTMLElement {
|
||||
const card = screen.getAllByText(name)
|
||||
.map((node) => node.closest(".plugin-builtins-item"))
|
||||
.find((node): node is HTMLElement => node instanceof HTMLElement);
|
||||
expect(card).toBeTruthy();
|
||||
return card;
|
||||
}
|
||||
|
||||
function expectEventsUrl(url: string, projectId?: string) {
|
||||
const parsed = new URL(url, "http://localhost");
|
||||
@@ -271,9 +281,79 @@ describe("PluginManager", () => {
|
||||
expect(screen.getByText("Reports")).toBeTruthy();
|
||||
expect(screen.getByText("WhatsApp Chat")).toBeTruthy();
|
||||
expect(screen.getByText("CLI Printing Press")).toBeTruthy();
|
||||
expect(screen.getByText("Linear Import")).toBeTruthy();
|
||||
expect(screen.getByText(/Pairs to WhatsApp Web \(multi-device\) with QR or pairing code/i)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("keeps Linear Import in the bundled plugin catalog", () => {
|
||||
expect(BUILTIN_PLUGINS.find((plugin) => plugin.id === LINEAR_PLUGIN_ID)).toMatchObject({
|
||||
id: LINEAR_PLUGIN_ID,
|
||||
name: "Linear Import",
|
||||
category: "integration",
|
||||
path: "./plugins/fusion-plugin-linear-import",
|
||||
description: "Browse Linear issues and import selected issues as Fusion triage tasks through plugin-owned settings.",
|
||||
});
|
||||
});
|
||||
|
||||
it("installs Linear Import from the built-in section when not installed", async () => {
|
||||
vi.mocked(fetchPlugins).mockResolvedValue([]);
|
||||
render(<PluginManager addToast={addToast} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetchPlugins).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const linearCard = getBuiltInPluginCard("Linear Import");
|
||||
expect(within(linearCard).getByText("Not installed")).toBeTruthy();
|
||||
const installButton = within(linearCard).getByRole("button", { name: /Install Linear Import/i });
|
||||
await userEvent.click(installButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(installPlugin).toHaveBeenCalledWith({ path: "./plugins/fusion-plugin-linear-import" }, undefined);
|
||||
expect(addToast).toHaveBeenCalledWith("Linear Import installed globally", "success");
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ state: "installed" as const, enabled: false, statusLabel: "installed" },
|
||||
{ state: "started" as const, enabled: true, statusLabel: "started" },
|
||||
{ state: "error" as const, enabled: true, statusLabel: "error" },
|
||||
])("shows Manage for installed Linear Import in $state state", async ({ state, enabled, statusLabel }) => {
|
||||
vi.mocked(fetchPlugins).mockResolvedValue([
|
||||
{
|
||||
...mockPlugins[0],
|
||||
id: LINEAR_PLUGIN_ID,
|
||||
name: "Linear Import",
|
||||
description: "Browse Linear issues and import selected issues as Fusion triage tasks through plugin-owned settings.",
|
||||
state,
|
||||
enabled,
|
||||
error: state === "error" ? "Linear plugin failed to start" : undefined,
|
||||
settingsSchema: {},
|
||||
},
|
||||
]);
|
||||
|
||||
render(<PluginManager addToast={addToast} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByText("Linear Import").length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
const linearCard = getBuiltInPluginCard("Linear Import");
|
||||
expect(within(linearCard).getByText("Installed")).toBeTruthy();
|
||||
expect(within(linearCard).queryByText("Built-in metadata only")).toBeNull();
|
||||
expect(within(linearCard).getAllByText("integration").length).toBeGreaterThanOrEqual(1);
|
||||
expect(screen.getAllByText(statusLabel).length).toBeGreaterThanOrEqual(1);
|
||||
|
||||
const manageButton = within(linearCard).getByRole("button", { name: /^Manage$/i });
|
||||
expect(manageButton).not.toBeDisabled();
|
||||
await userEvent.click(manageButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetchPluginSettings).toHaveBeenCalledWith(LINEAR_PLUGIN_ID, undefined);
|
||||
});
|
||||
expect(screen.getByTestId("plugin-manager-detail")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders built-in agent browser metadata-only entry when uninstalled", async () => {
|
||||
render(<PluginManager addToast={addToast} />);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user