diff --git a/apps/app/src/hooks/queries/plugin-settings-queries.ts b/apps/app/src/hooks/queries/plugin-settings-queries.ts index 2456b750a37..7e9d7bbc6a5 100644 --- a/apps/app/src/hooks/queries/plugin-settings-queries.ts +++ b/apps/app/src/hooks/queries/plugin-settings-queries.ts @@ -52,6 +52,7 @@ export interface PluginListItem { source: string; isOrphanedBuiltin: boolean; catalogEntryId: string | null; + catalogMarketplaceName: string | null; publisherLabel: string | null; sourceDisplay: string; updateState: PluginUpdateState; @@ -108,6 +109,7 @@ export function toPluginListItem(plugin: InstalledPlugin): PluginListItem { source: plugin.source, isOrphanedBuiltin: plugin.isOrphanedBuiltin, catalogEntryId: plugin.catalogEntryId ?? null, + catalogMarketplaceName: plugin.catalogMarketplaceName ?? null, publisherLabel: plugin.publisherLabel, sourceDisplay: plugin.sourceDisplay, updateState: { diff --git a/apps/app/src/test/fixtures/plugins.ts b/apps/app/src/test/fixtures/plugins.ts index eee2d172319..9e2053505cd 100644 --- a/apps/app/src/test/fixtures/plugins.ts +++ b/apps/app/src/test/fixtures/plugins.ts @@ -67,6 +67,7 @@ export function makePluginListItem( provenance: "direct", isOrphanedBuiltin: false, catalogEntryId: null, + catalogMarketplaceName: null, publisherLabel: null, sourceDisplay: "path ยท /plugins/plugin-test", updateState: { diff --git a/apps/app/src/views/ToolsView.plugin-detail.test.tsx b/apps/app/src/views/ToolsView.plugin-detail.test.tsx index 5059c307880..a3fac8194a0 100644 --- a/apps/app/src/views/ToolsView.plugin-detail.test.tsx +++ b/apps/app/src/views/ToolsView.plugin-detail.test.tsx @@ -589,6 +589,168 @@ describe("PluginDetail official catalog lifecycle", () => { }); describe("BB Official plugin detail routing", () => { + it("uses the installed catalog identity when plugin ids collide", async () => { + const firstCatalogEntry = { + ...GITHUB_CATALOG_ENTRY, + marketplace: "bb-community", + marketplaceDisplayName: "BB Community", + publisherKey: "bb-community:example", + publisherLabel: "BB Community", + description: "Description from the first catalog.", + repositoryUrl: "https://github.com/example/first-catalog-plugin", + author: { + name: "First publisher", + github: "first-publisher", + url: "https://github.com/first-publisher", + }, + }; + const installedCatalogEntry = { + ...GITHUB_CATALOG_ENTRY, + marketplace: "partner-catalog", + marketplaceDisplayName: "Partner Catalog", + publisherKey: "partner-catalog:example", + publisherLabel: "Partner Catalog", + description: "Description from the installed catalog.", + repositoryUrl: "https://github.com/example/installed-catalog-plugin", + author: { + name: "Installed publisher", + github: "installed-publisher", + url: "https://github.com/installed-publisher", + }, + installed: true, + }; + vi.stubGlobal( + "fetch", + vi.fn(async (input: RequestInfo | URL) => { + const url = String(input); + if (url === "/api/v1/plugins") { + return new Response( + JSON.stringify({ + enabled: true, + plugins: [ + { + ...GITHUB_PLUGIN, + catalogMarketplaceName: "partner-catalog", + publisherLabel: "Partner Catalog", + iconUrl: null, + screenshots: [], + collections: [], + providerIds: [], + icons: {}, + updateState: {}, + }, + ], + }), + { headers: { "content-type": "application/json" } }, + ); + } + if (url.startsWith("/api/v1/plugin-catalog/search")) { + return new Response( + JSON.stringify({ + results: [firstCatalogEntry, installedCatalogEntry], + collections: [], + }), + { headers: { "content-type": "application/json" } }, + ); + } + return new Response(JSON.stringify({ error: "not found" }), { + status: 404, + headers: { "content-type": "application/json" }, + }); + }), + ); + + const { wrapper: QueryClientWrapper } = createQueryClientTestHarness(); + render( + + + } /> + + , + { wrapper: QueryClientWrapper }, + ); + + await waitFor(() => { + expect( + document.querySelector("[data-plugin-summary]")?.textContent, + ).toBe("Description from the installed catalog."); + }); + }); + + it("uses installed metadata when its catalog entry is unavailable", async () => { + const unrelatedCatalogEntry = { + ...GITHUB_CATALOG_ENTRY, + marketplace: "bb-community", + marketplaceDisplayName: "BB Community", + publisherKey: "bb-community:example", + publisherLabel: "BB Community", + description: "Description from an unrelated catalog entry.", + repositoryUrl: "https://github.com/example/unrelated-catalog-plugin", + author: { + name: "Unrelated publisher", + github: "unrelated-publisher", + url: "https://github.com/unrelated-publisher", + }, + }; + vi.stubGlobal( + "fetch", + vi.fn(async (input: RequestInfo | URL) => { + const url = String(input); + if (url === "/api/v1/plugins") { + return new Response( + JSON.stringify({ + enabled: true, + plugins: [ + { + ...GITHUB_PLUGIN, + description: "Description from installed metadata.", + catalogMarketplaceName: "partner-catalog", + publisherLabel: "Partner Catalog", + iconUrl: null, + screenshots: [], + collections: [], + providerIds: [], + icons: {}, + updateState: {}, + }, + ], + }), + { headers: { "content-type": "application/json" } }, + ); + } + if (url.startsWith("/api/v1/plugin-catalog/search")) { + return new Response( + JSON.stringify({ + results: [unrelatedCatalogEntry], + collections: [], + }), + { headers: { "content-type": "application/json" } }, + ); + } + return new Response(JSON.stringify({ error: "not found" }), { + status: 404, + headers: { "content-type": "application/json" }, + }); + }), + ); + + const { wrapper: QueryClientWrapper } = createQueryClientTestHarness(); + render( + + + } /> + + , + { wrapper: QueryClientWrapper }, + ); + + await waitFor(() => { + expect( + document.querySelector("[data-plugin-summary]")?.textContent, + ).toBe("Description from installed metadata."); + }); + }); + it("resolves an uninstalled catalog plugin and opens its install confirmation", async () => { vi.stubGlobal( "fetch", @@ -763,6 +925,7 @@ describe("BB Official plugin detail routing", () => { plugins: [ { ...GITHUB_PLUGIN, + catalogMarketplaceName: "bb-official", iconUrl: null, screenshots: [], collections: [], diff --git a/apps/app/src/views/ToolsView.tsx b/apps/app/src/views/ToolsView.tsx index 765f45c48bc..056e5dc4d31 100644 --- a/apps/app/src/views/ToolsView.tsx +++ b/apps/app/src/views/ToolsView.tsx @@ -224,9 +224,23 @@ function PluginDetailToolView({ pluginId }: { pluginId: string }) { const isLoading = listQuery.isFetching && listQuery.data === undefined; const selectedPlugin = plugins.find((plugin) => plugin.id === pluginId) ?? null; + const selectedCatalogEntryId = selectedPlugin?.catalogEntryId ?? null; + const selectedCatalogMarketplaceName = + selectedPlugin?.catalogMarketplaceName ?? null; const selectedCatalogEntry = - catalogQuery.data?.entries.find((entry) => entry.pluginId === pluginId) ?? - null; + selectedPlugin === null + ? (catalogQuery.data?.entries.find( + (entry) => entry.pluginId === pluginId, + ) ?? null) + : selectedCatalogEntryId === null || + selectedCatalogMarketplaceName === null + ? null + : (catalogQuery.data?.entries.find( + (entry) => + entry.pluginId === selectedPlugin.id && + entry.entryId === selectedCatalogEntryId && + entry.marketplace === selectedCatalogMarketplaceName, + ) ?? null); useResourceRouteLabel( selectedPlugin?.name ?? selectedPlugin?.id ??