From b8ebb730de0b46b59335d1c4a877bc023d1e3edd Mon Sep 17 00:00:00 2001 From: Bersabel Tadesse Date: Wed, 2 Sep 2026 00:46:02 -0700 Subject: [PATCH 01/12] Unify customizable sidebar navigation --- .../plugin/PluginNavSidebarItems.test.tsx | 416 +++++++---- .../plugin/PluginNavSidebarItems.tsx | 678 +++++++++++++----- .../plugin/pluginNavSidebarAtoms.test.ts | 178 +++++ .../plugin/pluginNavSidebarAtoms.ts | 141 +++- .../plugin/pluginNavSidebarOrder.test.ts | 222 ++++-- .../plugin/pluginNavSidebarOrder.ts | 122 +++- .../sidebar/BuiltInSidebarNavigation.tsx | 140 +++- .../src/components/sidebar/ProjectList.tsx | 174 +++-- .../sidebar/ProjectListActionButtons.test.tsx | 57 ++ 9 files changed, 1636 insertions(+), 492 deletions(-) create mode 100644 apps/app/src/components/plugin/pluginNavSidebarAtoms.test.ts create mode 100644 apps/app/src/components/sidebar/ProjectListActionButtons.test.tsx diff --git a/apps/app/src/components/plugin/PluginNavSidebarItems.test.tsx b/apps/app/src/components/plugin/PluginNavSidebarItems.test.tsx index 55303fce25..342ccd0878 100644 --- a/apps/app/src/components/plugin/PluginNavSidebarItems.test.tsx +++ b/apps/app/src/components/plugin/PluginNavSidebarItems.test.tsx @@ -6,11 +6,13 @@ import { render, screen, waitFor, + within, } from "@testing-library/react"; import { useEffect, type ComponentType } from "react"; import { createStore, Provider } from "jotai"; -import { MemoryRouter } from "react-router-dom"; +import { MemoryRouter, useLocation } from "react-router-dom"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { CompactViewportOverrideProvider } from "@bb/shared-ui/hooks/use-compact-viewport"; import { SidebarProvider } from "@/components/ui/sidebar.js"; import { @@ -18,12 +20,20 @@ import { setPluginSlotRegistrations, type PluginRegistrationSet, } from "@/lib/plugin-slots"; +import { getPluginPanelRoutePath } from "@/lib/route-paths"; import { resetAllCrashedPluginSlotsForTest, resetCrashedPluginSlots, } from "./PluginSlotMount"; -import { PluginNavSidebarItems } from "./PluginNavSidebarItems"; -import { pluginNavPanelOrderAtom } from "./pluginNavSidebarAtoms"; +import { + type BuiltInSidebarNavEntry, + ExtensionsNavSidebarItem, + PluginNavSidebarItems, +} from "./PluginNavSidebarItems"; +import { + pluginNavPanelOrderAtom, + pluginNavVisiblePanelKeysAtom, +} from "./pluginNavSidebarAtoms"; function registrationSet( overrides: Partial, @@ -68,37 +78,83 @@ function registerPanel( function renderSidebarItems( options: { - toolsRoutePath?: string; + builtInEntries?: readonly BuiltInSidebarNavEntry[]; storedOrder?: string[]; + storedVisibleKeys?: string[] | null; compactViewport?: boolean; } = {}, ) { const store = createStore(); + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }); if (options.storedOrder) { store.set(pluginNavPanelOrderAtom, options.storedOrder); } - return render( + if ("storedVisibleKeys" in options) { + store.set( + pluginNavVisiblePanelKeysAtom, + options.storedVisibleKeys ?? null, + ); + } + const view = render( - - - - - - - + + + + + + + + + + , ); + return { ...view, store }; +} + +function LocationProbe() { + return {useLocation().pathname}; } -const ROW_LABELS = new Set(["Extensions", "Docs", "GitHub"]); +function panelRowNames(labels: readonly string[] = ["Docs", "GitHub"]): string[] { + const rowLabels = new Set(labels); + const container = screen.queryByTestId("plugin-nav-sidebar-items"); + if (!container) return []; + return Array.from( + container.querySelectorAll("[data-sidebar-navigation-item]"), + ) + .map((row) => row.textContent?.trim() ?? "") + .filter((label) => rowLabels.has(label)); +} -function panelRowNames(): string[] { - return screen - .getAllByRole("button") - .map((button) => button.textContent?.trim() ?? "") - .filter((label) => ROW_LABELS.has(label)); +function builtInEntry( + id: string, + title: string, + onActivate: () => void = vi.fn(), +): BuiltInSidebarNavEntry { + return { + kind: "built-in", + pluginId: "__bb__", + id, + title, + icon: + { + event.preventDefault(); + contentRef.current + ?.querySelector( + "[data-sidebar-navigation-customize-launch]", + ) + ?.focus(); + }} + > +
+ Sidebar navigation +
+
+ + + {rows.map((row) => { + const key = getPluginNavPanelKey(row); + return ( + { + onActivate(row); + onOpenChange(false); + }} + onCheckedChange={(checked) => + onVisibleChange(key, checked) + } + /> + ); + })} + + +
+
+ + ); +} + +function SortableSidebarNavigationCustomizeItem({ + checked, + onActivate, + onCheckedChange, + reorderDisabled, + row, +}: { + checked: boolean; + onActivate: () => void; + onCheckedChange: (checked: boolean) => void; + reorderDisabled: boolean; + row: SidebarNavRow; +}) { + const panelKey = getPluginNavPanelKey(row); + const { dragBindings, setNodeRef, style } = useSidebarSortable({ + id: panelKey, + disabled: reorderDisabled, + }); + const icon = isPluginSidebarNavRow(row) ? ( + + ) : ( + row.icon + ); + + return ( +
-
); } @@ -307,13 +603,11 @@ const SortableSidebarNavRow = function SortableSidebarNavRow({ }; interface SidebarNavRowItemProps { - row: SidebarNavRow; + row: PluginSidebarNavRow; pathname: string; onNavigate?: () => void; splitEnabled: boolean; - isHidden?: boolean; onHide?: (key: string) => void; - onShow?: (key: string) => void; dragBindings?: SidebarSortableDragBindings; rowRef?: (element: HTMLElement | null) => void; rowStyle?: CSSProperties; @@ -324,9 +618,7 @@ function SidebarNavRowItem({ splitEnabled, ...props }: SidebarNavRowItemProps) { - return row.kind === "tools" ? ( - - ) : ( + return ( ); } @@ -334,18 +626,16 @@ function SidebarNavRowItem({ type PluginNavRowMenuSurface = "context" | "dropdown"; function PluginNavRowVisibilityMenuItem({ - isHidden, onSelect, surface, }: { - isHidden: boolean; onSelect: () => void; surface: PluginNavRowMenuSurface; }) { const content = ( <> -