diff --git a/.changeset/fn-7454-linear-plugin-visibility.md b/.changeset/fn-7454-linear-plugin-visibility.md new file mode 100644 index 0000000000..38b7690719 --- /dev/null +++ b/.changeset/fn-7454-linear-plugin-visibility.md @@ -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. diff --git a/packages/dashboard/app/components/PluginManager.tsx b/packages/dashboard/app/components/PluginManager.tsx index 0bf4448334..5061d0fffa 100644 --- a/packages/dashboard/app/components/PluginManager.tsx +++ b/packages/dashboard/app/components/PluginManager.tsx @@ -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", diff --git a/packages/dashboard/app/components/__tests__/PluginManager.registry.test.tsx b/packages/dashboard/app/components/__tests__/PluginManager.registry.test.tsx index 8757cc0d4e..0622d8efc0 100644 --- a/packages/dashboard/app/components/__tests__/PluginManager.registry.test.tsx +++ b/packages/dashboard/app/components/__tests__/PluginManager.registry.test.tsx @@ -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(); 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); diff --git a/packages/dashboard/app/components/__tests__/PluginManager.test.tsx b/packages/dashboard/app/components/__tests__/PluginManager.test.tsx index b7d8d6bd5d..2d274398d2 100644 --- a/packages/dashboard/app/components/__tests__/PluginManager.test.tsx +++ b/packages/dashboard/app/components/__tests__/PluginManager.test.tsx @@ -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(); + + 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(); + + 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();