diff --git a/apps/app/src/components/layout/AppLayout.plugin-panel-header.test.tsx b/apps/app/src/components/layout/AppLayout.plugin-panel-header.test.tsx index 7dff45cb33..ba54eb2796 100644 --- a/apps/app/src/components/layout/AppLayout.plugin-panel-header.test.tsx +++ b/apps/app/src/components/layout/AppLayout.plugin-panel-header.test.tsx @@ -28,6 +28,7 @@ vi.mock("@/hooks/queries/system-queries", () => ({ data: { experiments: { claudeCodeMockCliTraffic: false, + cloudAi: false, newOnboarding: false, toolsHub: true, }, diff --git a/apps/app/src/components/layout/AppLayout.root-compose-project.test.tsx b/apps/app/src/components/layout/AppLayout.root-compose-project.test.tsx index b2f91fa6c8..98e8cebdf2 100644 --- a/apps/app/src/components/layout/AppLayout.root-compose-project.test.tsx +++ b/apps/app/src/components/layout/AppLayout.root-compose-project.test.tsx @@ -24,6 +24,7 @@ vi.mock("@/hooks/queries/system-queries", () => ({ data: { experiments: { claudeCodeMockCliTraffic: false, + cloudAi: false, newOnboarding: false, toolsHub: true, }, diff --git a/apps/app/src/components/plugin/PluginSettings.test.tsx b/apps/app/src/components/plugin/PluginSettings.test.tsx index 68d1ddbdaa..60973357d7 100644 --- a/apps/app/src/components/plugin/PluginSettings.test.tsx +++ b/apps/app/src/components/plugin/PluginSettings.test.tsx @@ -1,6 +1,7 @@ // @vitest-environment jsdom import { cleanup, fireEvent, render, screen } from "@testing-library/react"; +import { MemoryRouter } from "react-router-dom"; import { afterEach, describe, expect, it, vi } from "vitest"; import { createQueryClientTestHarness } from "@/test/queryClientTestHarness"; import { @@ -283,14 +284,16 @@ describe("PluginSettingsDetail settings gating", () => { }); const { wrapper } = createQueryClientTestHarness(); render( - , + + + , { wrapper }, ); diff --git a/apps/app/src/components/plugin/PluginSettingsSections.tsx b/apps/app/src/components/plugin/PluginSettingsSections.tsx index d1242e41f5..33cba5e479 100644 --- a/apps/app/src/components/plugin/PluginSettingsSections.tsx +++ b/apps/app/src/components/plugin/PluginSettingsSections.tsx @@ -1,3 +1,6 @@ +import { useEffect } from "react"; +import { useLocation } from "react-router-dom"; +import { useSystemConfig } from "@/hooks/queries/system-queries"; import { usePluginSlots, type PluginSettingsSectionSlot, @@ -8,6 +11,20 @@ import { ResourceDetailConfigurationSection, } from "@bb/shared-ui/resource-list"; +const CONNECT_PLUGIN_ID = "connect"; +const CLOUD_AI_SECTION_ID = "cloud-ai"; + +function isSettingsSectionVisible( + section: PluginSettingsSectionSlot, + cloudAiEnabled: boolean, +): boolean { + return !( + section.pluginId === CONNECT_PLUGIN_ID && + section.id === CLOUD_AI_SECTION_ID && + !cloudAiEnabled + ); +} + /** * Plugin `settingsSection` slot mounts, rendered on that plugin's canonical * Plugins detail page below the host-rendered declarative form. @@ -15,8 +32,12 @@ import { */ export function PluginSettingsSections({ pluginId }: { pluginId: string }) { const { settingsSections } = usePluginSlots(); + const cloudAiEnabled = + useSystemConfig().data?.experiments?.cloudAi === true; const sections = settingsSections.filter( - (section) => section.pluginId === pluginId, + (section) => + section.pluginId === pluginId && + isSettingsSectionVisible(section, cloudAiEnabled), ); if (sections.length === 0) return null; return ; @@ -27,28 +48,47 @@ function PluginSettingsSectionList({ }: { sections: readonly PluginSettingsSectionSlot[]; }) { + const location = useLocation(); + + useEffect(() => { + if (location.hash.length <= 1) return; + let sectionId: string; + try { + sectionId = decodeURIComponent(location.hash.slice(1)); + } catch { + return; + } + if (!sections.some((section) => section.id === sectionId)) return; + document.getElementById(sectionId)?.scrollIntoView({ block: "start" }); + }, [location.hash, location.key, sections]); + return (
{sections.map((section) => ( - - - {section.description !== undefined ? ( -

- {section.description} -

- ) : null} - - - -
-
+ + + {section.description !== undefined ? ( +

+ {section.description} +

+ ) : null} + + + +
+
+
))} ); diff --git a/apps/app/src/components/plugin/PluginSidebarFooterActions.test.tsx b/apps/app/src/components/plugin/PluginSidebarFooterActions.test.tsx index 9ba8114110..a852460af8 100644 --- a/apps/app/src/components/plugin/PluginSidebarFooterActions.test.tsx +++ b/apps/app/src/components/plugin/PluginSidebarFooterActions.test.tsx @@ -32,7 +32,13 @@ function registrationSet( } function LocationProbe() { - return {useLocation().pathname}; + const location = useLocation(); + return ( + + {location.pathname} + {location.hash} + + ); } function renderWithProviders(ui: ReactNode, toolsHubEnabled = false) { @@ -56,7 +62,7 @@ afterEach(() => { }); describe("PluginSidebarFooterActions", () => { - it("prefers branding.icon over the logo and contribution icon", () => { + it("uses the action icon instead of the plugin branding icon", () => { setPluginLogoUrls( new Map([ [ @@ -87,8 +93,8 @@ describe("PluginSidebarFooterActions", () => { renderWithProviders(); - expect(document.querySelector('[data-icon="FileText"]')).not.toBeNull(); - expect(document.querySelector('[data-icon="Smartphone"]')).toBeNull(); + expect(document.querySelector('[data-icon="Smartphone"]')).not.toBeNull(); + expect(document.querySelector('[data-icon="FileText"]')).toBeNull(); expect(document.querySelector("img")).toBeNull(); }); @@ -143,4 +149,28 @@ describe("PluginSidebarFooterActions", () => { ); }, ); + + it("opens a specific plugin settings section", () => { + setPluginSlotRegistrations( + "cloud", + registrationSet({ + sidebarFooterActions: [ + { + id: "remote-access", + title: "Remote access", + icon: "Smartphone", + run: ({ openSettings }) => + openSettings({ sectionId: "remote-access" }), + }, + ], + }), + ); + + renderWithProviders(); + fireEvent.click(screen.getByRole("button", { name: "Remote access" })); + + expect(screen.getByLabelText("Current path").textContent).toBe( + "/settings/plugins/cloud#remote-access", + ); + }); }); diff --git a/apps/app/src/components/plugin/PluginSidebarFooterActions.tsx b/apps/app/src/components/plugin/PluginSidebarFooterActions.tsx index 49e253a954..186b5e1bd6 100644 --- a/apps/app/src/components/plugin/PluginSidebarFooterActions.tsx +++ b/apps/app/src/components/plugin/PluginSidebarFooterActions.tsx @@ -1,8 +1,9 @@ import { useNavigate } from "react-router-dom"; import { cn } from "@bb/shared-ui/lib/utils"; import { COARSE_POINTER_CHILD_ICON_BUTTON_CLASS } from "@bb/shared-ui/coarse-pointer-sizing"; +import { Icon } from "@bb/shared-ui/icon"; import { SidebarMenuButton, SidebarMenuItem } from "@/components/ui/sidebar.js"; -import { PluginIcon } from "@/components/plugin/PluginIcon"; +import { pluginIconName } from "@/components/plugin/PluginIcon"; import { usePluginSlots, type PluginSidebarFooterActionSlot, @@ -63,7 +64,11 @@ function PluginSidebarFooterActionList({ }); }} > - +