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",
|
category: "integration",
|
||||||
path: "./plugins/fusion-plugin-compound-engineering",
|
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,
|
id: BUILTIN_AGENT_BROWSER_PLUGIN_ID,
|
||||||
name: "Agent Browser",
|
name: "Agent Browser",
|
||||||
|
|||||||
@@ -97,8 +97,8 @@ function stubEventSource() {
|
|||||||
vi.stubGlobal("EventSource", MockEventSource);
|
vi.stubGlobal("EventSource", MockEventSource);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renderRegistry(entries: RegistryPluginEntry[] = registryEntries) {
|
async function renderRegistry(entries: RegistryPluginEntry[] = registryEntries, installed: PluginInstallation[] = [installedPlugin]) {
|
||||||
vi.mocked(fetchPlugins).mockResolvedValue([installedPlugin]);
|
vi.mocked(fetchPlugins).mockResolvedValue(installed);
|
||||||
vi.mocked(fetchPluginRegistry).mockResolvedValue(entries);
|
vi.mocked(fetchPluginRegistry).mockResolvedValue(entries);
|
||||||
render(<PluginManager addToast={addToast} />);
|
render(<PluginManager addToast={addToast} />);
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
@@ -151,6 +151,74 @@ describe("PluginManager registry browsing", () => {
|
|||||||
expect(within(comingSoon).getByText("Coming Soon")).toBeInTheDocument();
|
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 () => {
|
it("installs registry plugins with their manifest path and refreshes installed plugins", async () => {
|
||||||
await renderRegistry();
|
await renderRegistry();
|
||||||
expect(fetchPlugins).toHaveBeenCalledTimes(1);
|
expect(fetchPlugins).toHaveBeenCalledTimes(1);
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ vi.mock("../../hooks/useConfirm", () => ({
|
|||||||
import {
|
import {
|
||||||
AGENT_BROWSER_SETTINGS_SCHEMA,
|
AGENT_BROWSER_SETTINGS_SCHEMA,
|
||||||
BUILTIN_AGENT_BROWSER_PLUGIN_ID,
|
BUILTIN_AGENT_BROWSER_PLUGIN_ID,
|
||||||
|
BUILTIN_PLUGINS,
|
||||||
PluginManager,
|
PluginManager,
|
||||||
STATE_COLORS,
|
STATE_COLORS,
|
||||||
} from "../PluginManager";
|
} from "../PluginManager";
|
||||||
@@ -131,6 +132,15 @@ import {
|
|||||||
} from "../../api";
|
} from "../../api";
|
||||||
|
|
||||||
const addToast = vi.fn();
|
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) {
|
function expectEventsUrl(url: string, projectId?: string) {
|
||||||
const parsed = new URL(url, "http://localhost");
|
const parsed = new URL(url, "http://localhost");
|
||||||
@@ -271,9 +281,79 @@ describe("PluginManager", () => {
|
|||||||
expect(screen.getByText("Reports")).toBeTruthy();
|
expect(screen.getByText("Reports")).toBeTruthy();
|
||||||
expect(screen.getByText("WhatsApp Chat")).toBeTruthy();
|
expect(screen.getByText("WhatsApp Chat")).toBeTruthy();
|
||||||
expect(screen.getByText("CLI Printing Press")).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();
|
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 () => {
|
it("renders built-in agent browser metadata-only entry when uninstalled", async () => {
|
||||||
render(<PluginManager addToast={addToast} />);
|
render(<PluginManager addToast={addToast} />);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user