From c994064f0845e24321605ce14d0e8ddfd40d2a0f Mon Sep 17 00:00:00 2001 From: Peter Kirkham Date: Wed, 22 Jul 2026 09:58:06 +0200 Subject: [PATCH 1/3] Fix duplicate configuration navigation Generated-By: PostHog Code Task-Id: 884159da-252c-4037-ab49-c6178d27732e --- .../CustomizeSidebarDialog.test.tsx | 43 ++++++++--------- .../components/CustomizeSidebarDialog.tsx | 6 --- .../components/SidebarNavSection.test.tsx | 8 +--- .../sidebar/components/SidebarNavSection.tsx | 46 +------------------ .../ui/src/features/sidebar/constants.test.ts | 24 ++++++---- packages/ui/src/features/sidebar/constants.ts | 18 -------- .../src/features/sidebar/sidebarStore.test.ts | 4 +- 7 files changed, 41 insertions(+), 108 deletions(-) diff --git a/packages/ui/src/features/sidebar/components/CustomizeSidebarDialog.test.tsx b/packages/ui/src/features/sidebar/components/CustomizeSidebarDialog.test.tsx index b94f3f3458..10989f0315 100644 --- a/packages/ui/src/features/sidebar/components/CustomizeSidebarDialog.test.tsx +++ b/packages/ui/src/features/sidebar/components/CustomizeSidebarDialog.test.tsx @@ -117,13 +117,13 @@ describe("CustomizeSidebarDialog", () => { const user = userEvent.setup(); renderDialog(); - await user.click(screen.getByRole("checkbox", { name: "MCP servers" })); + await user.click(screen.getByRole("checkbox", { name: "Command Center" })); - expect(useSidebarStore.getState().navItemOverrides["mcp-servers"]).toBe( + expect(useSidebarStore.getState().navItemOverrides["command-center"]).toBe( false, ); expect(track).toHaveBeenCalledWith(ANALYTICS_EVENTS.SIDEBAR_CUSTOMIZED, { - item: "mcp_servers", + item: "command_center", visible: false, }); }); @@ -162,29 +162,26 @@ describe("CustomizeSidebarDialog", () => { it("previews on dragover and persists only on drop", () => { renderDialog(); - dragStart("skills"); - dragOver("skills", "search"); + dragStart("loops"); + dragOver("loops", "search"); - expect(rowLabels()[0]).toBe("Skills"); + expect(rowLabels()[0]).toBe("Loops"); expect(useSidebarStore.getState().navItemOrder).toEqual([]); expect(track).not.toHaveBeenCalled(); - dragEnd("skills"); + dragEnd("loops"); expect(useSidebarStore.getState().navItemOrder).toEqual([ - "skills", + "loops", "search", "inbox", - "agents", - "loops", - "mcp-servers", "command-center", "contexts", "activity", "configure", ]); expect(track).toHaveBeenCalledWith(ANALYTICS_EVENTS.SIDEBAR_REORDERED, { - item: "skills", + item: "loops", to_index: 0, }); }); @@ -192,23 +189,23 @@ describe("CustomizeSidebarDialog", () => { it("ignores a repeated dragover for the same source and target", () => { renderDialog(); - dragStart("skills"); - dragOver("skills", "search"); - dragOver("skills", "search"); + dragStart("loops"); + dragOver("loops", "search"); + dragOver("loops", "search"); - expect(rowLabels()[0]).toBe("Skills"); + expect(rowLabels()[0]).toBe("Loops"); - dragEnd("skills"); + dragEnd("loops"); - expect(useSidebarStore.getState().navItemOrder[0]).toBe("skills"); + expect(useSidebarStore.getState().navItemOrder[0]).toBe("loops"); }); it("a canceled drag drops the preview and leaves the store untouched", () => { renderDialog(); - dragStart("skills"); - dragOver("skills", "search"); - dragEnd("skills", { cancel: true }); + dragStart("loops"); + dragOver("loops", "search"); + dragEnd("loops", { cancel: true }); expect(rowLabels()[0]).toBe("Search"); expect(useSidebarStore.getState().navItemOrder).toEqual([]); @@ -218,8 +215,8 @@ describe("CustomizeSidebarDialog", () => { it("a drop without movement neither persists nor tracks", () => { renderDialog(); - dragStart("skills"); - dragEnd("skills"); + dragStart("loops"); + dragEnd("loops"); expect(useSidebarStore.getState().navItemOrder).toEqual([]); expect(track).not.toHaveBeenCalled(); diff --git a/packages/ui/src/features/sidebar/components/CustomizeSidebarDialog.tsx b/packages/ui/src/features/sidebar/components/CustomizeSidebarDialog.tsx index 924448c69d..3be20438c5 100644 --- a/packages/ui/src/features/sidebar/components/CustomizeSidebarDialog.tsx +++ b/packages/ui/src/features/sidebar/components/CustomizeSidebarDialog.tsx @@ -5,12 +5,9 @@ import { DotsSixVertical, EnvelopeSimple, HashIcon, - Lightbulb, Lightning, MagnifyingGlass, - Plugs, RepeatIcon, - Robot, SlidersHorizontal, } from "@phosphor-icons/react"; import { ANALYTICS_EVENTS } from "@posthog/shared/analytics-events"; @@ -33,9 +30,6 @@ const ITEM_ICONS: Record< > = { search: MagnifyingGlass, inbox: EnvelopeSimple, - agents: Robot, - skills: Lightbulb, - "mcp-servers": Plugs, "command-center": Lightning, contexts: HashIcon, activity: Bell, diff --git a/packages/ui/src/features/sidebar/components/SidebarNavSection.test.tsx b/packages/ui/src/features/sidebar/components/SidebarNavSection.test.tsx index e0ca40bd39..2172453dc5 100644 --- a/packages/ui/src/features/sidebar/components/SidebarNavSection.test.tsx +++ b/packages/ui/src/features/sidebar/components/SidebarNavSection.test.tsx @@ -106,9 +106,6 @@ describe("SidebarNavSection", () => { it.each([ ["search", "Search"], ["inbox", "Inbox"], - ["agents", "Agents"], - ["skills", "Skills"], - ["mcp-servers", "MCP servers"], ["command-center", "Command Center"], ["contexts", "Channels"], ["activity", "Activity"], @@ -131,9 +128,6 @@ describe("SidebarNavSection", () => { it.each([ ["inbox", "inbox", "Inbox"], - ["agents", "agents", "Agents"], - ["skills", "skills", "Skills"], - ["mcp-servers", "mcp-servers", "MCP servers"], ["command-center", "command-center", "Command Center"], ["activity", "activity", "Activity"], ["loops", "loops", "Loops"], @@ -162,7 +156,7 @@ describe("SidebarNavSection", () => { labels.findIndex((text) => text.includes(label)); expect(position("Activity")).toBeLessThan(position("Inbox")); - expect(position("Inbox")).toBeLessThan(position("Agents")); + expect(position("Inbox")).toBeLessThan(position("Loops")); }); it("never lets hidden search take over the More row", () => { diff --git a/packages/ui/src/features/sidebar/components/SidebarNavSection.tsx b/packages/ui/src/features/sidebar/components/SidebarNavSection.tsx index 9d167a9c77..e8feebe566 100644 --- a/packages/ui/src/features/sidebar/components/SidebarNavSection.tsx +++ b/packages/ui/src/features/sidebar/components/SidebarNavSection.tsx @@ -18,17 +18,12 @@ import { useSidebarStore } from "@posthog/ui/features/sidebar/sidebarStore"; import { useTasks } from "@posthog/ui/features/tasks/useTasks"; import { navigateToActivity, - navigateToAgents, navigateToCommandCenter, navigateToHome, navigateToInbox, navigateToLoops, - navigateToMcpServers, - navigateToSkills, navigateToWebsiteCommandCenter, navigateToWebsiteHome, - navigateToWebsiteMcpServers, - navigateToWebsiteSkills, } from "@posthog/ui/router/navigationBridge"; import { useAppView } from "@posthog/ui/router/useAppView"; import { openTaskInput } from "@posthog/ui/router/useOpenTask"; @@ -39,7 +34,6 @@ import { useRouterState } from "@tanstack/react-router"; import { Fragment, type ReactNode, useState } from "react"; import { CustomizeSidebarDialog } from "./CustomizeSidebarDialog"; import { ActivityItem } from "./items/ActivityItem"; -import { AgentsItem } from "./items/AgentsItem"; import { CommandCenterItem } from "./items/CommandCenterItem"; import { ConfigureItem } from "./items/ConfigureItem"; import { ContextsItem } from "./items/ContextsItem"; @@ -47,11 +41,9 @@ import { CustomizeSidebarItem } from "./items/CustomizeSidebarItem"; import { HomeItem } from "./items/HomeItem"; import { InboxItem } from "./items/InboxItem"; import { LoopsItem } from "./items/LoopsItem"; -import { McpServersItem } from "./items/McpServersItem"; import { MoreItem } from "./items/MoreItem"; import { NewTaskItem } from "./items/NewTaskItem"; import { SearchItem } from "./items/SearchItem"; -import { SkillsItem } from "./items/SkillsItem"; const SIDEBAR_INBOX_REFETCH_INTERVAL_MS = 60_000; @@ -68,8 +60,8 @@ interface SidebarNavSectionProps { // and the Channels pane. It is fully self-contained — every item's active // state, badge count, and click handler is wired here — so it can be dropped // into either layout. In the Channels space, destinations with a /website -// mirror (Home, Skills, MCP servers, Command Center) stay in that space; -// Inbox, Agents and New task have no mirror yet and jump back to Code. +// mirror (Home and Command Center) stay in that space; Inbox and New task have +// no mirror yet and jump back to Code. // Configure opens the shared settings UI. Search opens the command menu in // place and defaults to the collapsible More row; the Customize sidebar // dialog controls which items show at the top level. @@ -101,10 +93,6 @@ export function SidebarNavSection({ const goNewTask = () => openTaskInput(inChannels ? { space: "website" } : undefined); const goHome = inChannels ? navigateToWebsiteHome : navigateToHome; - const goSkills = inChannels ? navigateToWebsiteSkills : navigateToSkills; - const goMcpServers = inChannels - ? navigateToWebsiteMcpServers - : navigateToMcpServers; const goCommandCenter = inChannels ? navigateToWebsiteCommandCenter : navigateToCommandCenter; @@ -116,11 +104,8 @@ export function SidebarNavSection({ const isHomeViewActive = view.type === "home"; const isActivityActive = view.type === "activity"; const isInboxActive = view.type === "inbox"; - const isAgentsActive = view.type === "agents"; const isLoopsActive = view.type === "loops"; const isCommandCenterActive = view.type === "command-center"; - const isSkillsActive = view.type === "skills"; - const isMcpServersActive = view.type === "mcp-servers"; // Open pull requests in the inbox — the main CTA, and the same count the inbox // Pull requests tab shows, so the badge and the tab always agree. @@ -183,9 +168,6 @@ export function SidebarNavSection({ const moreItemActive: Record = { search: false, inbox: isInboxActive, - agents: isAgentsActive, - skills: isSkillsActive, - "mcp-servers": isMcpServersActive, "command-center": isCommandCenterActive, contexts: false, activity: isActivityActive, @@ -196,9 +178,6 @@ export function SidebarNavSection({ const navItemAvailable: Record = { search: true, inbox: true, - agents: true, - skills: true, - "mcp-servers": true, "command-center": true, contexts: bluebirdEnabled, // Activity (the mentions feed) is a channels surface, so it only appears @@ -252,27 +231,6 @@ export function SidebarNavSection({ pullRequestCount={inboxPullRequestCount} /> ), - agents: (depth) => ( - - ), - skills: (depth) => ( - - ), - "mcp-servers": (depth) => ( - - ), "command-center": (depth) => ( { + it("keeps configuration destinations out of the top-level navigation", () => { + expect(CUSTOMIZABLE_NAV_ITEM_IDS).not.toEqual( + expect.arrayContaining(["agents", "skills", "mcp-servers"]), + ); + }); +}); + describe("orderedNavItems", () => { it("returns the default order for an empty stored order", () => { expect(orderedNavItems([]).map((item) => item.id)).toEqual( @@ -14,13 +22,13 @@ describe("orderedNavItems", () => { }); it("inserts an id missing from a full stored order after its default predecessor", () => { - const withoutSkills = CUSTOMIZABLE_NAV_ITEM_IDS.filter( - (id) => id !== "skills", + const withoutLoops = CUSTOMIZABLE_NAV_ITEM_IDS.filter( + (id) => id !== "loops", ).reverse(); - const ids = orderedNavItems(withoutSkills).map((item) => item.id); + const ids = orderedNavItems(withoutLoops).map((item) => item.id); - expect(ids.indexOf("skills")).toBe(ids.indexOf("agents") + 1); + expect(ids.indexOf("loops")).toBe(ids.indexOf("inbox") + 1); }); it("inserts a missing id with no present predecessor at the start", () => { @@ -43,17 +51,17 @@ describe("orderedNavItems", () => { describe("moveNavItem", () => { it("moves an item backward to the target position", () => { - const next = moveNavItem([], "skills", "search"); + const next = moveNavItem([], "loops", "search"); - expect(next[0]).toBe("skills"); + expect(next[0]).toBe("loops"); expect(next).toHaveLength(CUSTOMIZABLE_NAV_ITEM_IDS.length); }); it("moves an item forward to the target position", () => { - const next = moveNavItem([], "search", "agents"); + const next = moveNavItem([], "search", "loops"); expect(next.indexOf("search")).toBe( - CUSTOMIZABLE_NAV_ITEM_IDS.indexOf("agents"), + CUSTOMIZABLE_NAV_ITEM_IDS.indexOf("loops"), ); }); diff --git a/packages/ui/src/features/sidebar/constants.ts b/packages/ui/src/features/sidebar/constants.ts index 58bfb48ddd..8011ae2429 100644 --- a/packages/ui/src/features/sidebar/constants.ts +++ b/packages/ui/src/features/sidebar/constants.ts @@ -10,30 +10,12 @@ export const CUSTOMIZABLE_NAV_ITEMS = [ defaultVisible: false, }, { id: "inbox", label: "Inbox", analyticsId: "inbox", defaultVisible: true }, - { - id: "agents", - label: "Agents", - analyticsId: "agents", - defaultVisible: true, - }, - { - id: "skills", - label: "Skills", - analyticsId: "skills", - defaultVisible: true, - }, { id: "loops", label: "Loops", analyticsId: "loops", defaultVisible: true, }, - { - id: "mcp-servers", - label: "MCP servers", - analyticsId: "mcp_servers", - defaultVisible: true, - }, { id: "command-center", label: "Command Center", diff --git a/packages/ui/src/features/sidebar/sidebarStore.test.ts b/packages/ui/src/features/sidebar/sidebarStore.test.ts index 87b9bc4450..b942b1e45e 100644 --- a/packages/ui/src/features/sidebar/sidebarStore.test.ts +++ b/packages/ui/src/features/sidebar/sidebarStore.test.ts @@ -32,11 +32,11 @@ describe("sidebarStore navItemOverrides", () => { ); it("overriding one item leaves the others at their defaults", () => { - useSidebarStore.getState().setNavItemVisible("agents", false); + useSidebarStore.getState().setNavItemVisible("inbox", false); const overrides = useSidebarStore.getState().navItemOverrides; for (const item of CUSTOMIZABLE_NAV_ITEMS) { - if (item.id === "agents") continue; + if (item.id === "inbox") continue; expect(isNavItemVisible(overrides, item.id)).toBe(item.defaultVisible); } }); From 96f0ca6d3f61f93e7df89f583070583630c3f9de Mon Sep 17 00:00:00 2001 From: Peter Kirkham Date: Wed, 22 Jul 2026 10:14:40 +0200 Subject: [PATCH 2/3] Simplify sidebar customization Generated-By: PostHog Code Task-Id: 884159da-252c-4037-ab49-c6178d27732e --- .../components/SettingsPageContent.tsx | 2 + .../settings/components/SettingsPanel.tsx | 2 + .../ui/src/features/settings/types.test.ts | 8 ++ packages/ui/src/features/settings/types.ts | 2 + .../CustomizeSidebarDialog.test.tsx | 80 ++++--------- .../components/CustomizeSidebarDialog.tsx | 109 +++++++----------- .../components/SidebarNavSection.test.tsx | 69 +++-------- .../sidebar/components/SidebarNavSection.tsx | 74 +----------- .../components/items/CustomizeSidebarItem.tsx | 21 ---- .../sidebar/components/items/MoreItem.tsx | 32 ----- .../ui/src/features/sidebar/constants.test.ts | 36 +++--- packages/ui/src/features/sidebar/constants.ts | 6 - .../src/features/sidebar/sidebarStore.test.ts | 11 +- 13 files changed, 125 insertions(+), 327 deletions(-) create mode 100644 packages/ui/src/features/settings/types.test.ts delete mode 100644 packages/ui/src/features/sidebar/components/items/CustomizeSidebarItem.tsx delete mode 100644 packages/ui/src/features/sidebar/components/items/MoreItem.tsx diff --git a/packages/ui/src/features/settings/components/SettingsPageContent.tsx b/packages/ui/src/features/settings/components/SettingsPageContent.tsx index 4afe68ab67..355c89583b 100644 --- a/packages/ui/src/features/settings/components/SettingsPageContent.tsx +++ b/packages/ui/src/features/settings/components/SettingsPageContent.tsx @@ -17,6 +17,7 @@ import { UpdatesSettings } from "@posthog/ui/features/settings/sections/UpdatesS import { WorkspacesSettings } from "@posthog/ui/features/settings/sections/WorkspacesSettings"; import { WorktreesSettings } from "@posthog/ui/features/settings/sections/worktrees/WorktreesSettings"; import type { SettingsCategory } from "@posthog/ui/features/settings/types"; +import { CustomizeSidebarSettings } from "@posthog/ui/features/sidebar/components/CustomizeSidebarDialog"; import { SkillsView } from "@posthog/ui/features/skills/SkillsView"; import { Box, Flex, ScrollArea, Text } from "@radix-ui/themes"; import type { ComponentType, ReactNode } from "react"; @@ -69,6 +70,7 @@ const SETTINGS_PAGES: Record = { "Personalization", PersonalizationSettings, ), + sidebar: defineSettingsPage("Sidebar", CustomizeSidebarSettings), terminal: defineSettingsPage("Terminal", TerminalSettings), "claude-code": defineSettingsPage("Claude Code", ClaudeCodeSettings), shortcuts: defineSettingsPage("Shortcuts", ShortcutsSettings), diff --git a/packages/ui/src/features/settings/components/SettingsPanel.tsx b/packages/ui/src/features/settings/components/SettingsPanel.tsx index 5949d40d34..931f5e92dc 100644 --- a/packages/ui/src/features/settings/components/SettingsPanel.tsx +++ b/packages/ui/src/features/settings/components/SettingsPanel.tsx @@ -15,6 +15,7 @@ import { Palette, Plugs, Robot, + SidebarSimple, SignOut, SlackLogo, Terminal, @@ -101,6 +102,7 @@ const SIDEBAR_GROUPS: SidebarGroup[] = [ label: "Personalization", icon: , }, + { id: "sidebar", label: "Sidebar", icon: }, { id: "terminal", label: "Terminal", icon: }, { id: "shortcuts", label: "Shortcuts", icon: }, ], diff --git a/packages/ui/src/features/settings/types.test.ts b/packages/ui/src/features/settings/types.test.ts new file mode 100644 index 0000000000..d992d2930f --- /dev/null +++ b/packages/ui/src/features/settings/types.test.ts @@ -0,0 +1,8 @@ +import { describe, expect, it } from "vitest"; +import { isSettingsCategory } from "./types"; + +describe("isSettingsCategory", () => { + it("recognizes sidebar experience settings", () => { + expect(isSettingsCategory("sidebar")).toBe(true); + }); +}); diff --git a/packages/ui/src/features/settings/types.ts b/packages/ui/src/features/settings/types.ts index e549aba42f..5a6d06ff50 100644 --- a/packages/ui/src/features/settings/types.ts +++ b/packages/ui/src/features/settings/types.ts @@ -10,6 +10,7 @@ export type SettingsCategory = | "skills" | "mcp-servers" | "personalization" + | "sidebar" | "terminal" | "claude-code" | "shortcuts" @@ -32,6 +33,7 @@ export const SETTINGS_CATEGORIES: readonly SettingsCategory[] = [ "skills", "mcp-servers", "personalization", + "sidebar", "terminal", "claude-code", "shortcuts", diff --git a/packages/ui/src/features/sidebar/components/CustomizeSidebarDialog.test.tsx b/packages/ui/src/features/sidebar/components/CustomizeSidebarDialog.test.tsx index 10989f0315..a57bfb3280 100644 --- a/packages/ui/src/features/sidebar/components/CustomizeSidebarDialog.test.tsx +++ b/packages/ui/src/features/sidebar/components/CustomizeSidebarDialog.test.tsx @@ -45,32 +45,13 @@ vi.mock("@dnd-kit/react/sortable", () => ({ }), })); -import { - CUSTOMIZABLE_NAV_ITEM_IDS, - type CustomizableNavItemId, -} from "@posthog/ui/features/sidebar/constants"; import { useSidebarStore } from "@posthog/ui/features/sidebar/sidebarStore"; -import { CustomizeSidebarDialog } from "./CustomizeSidebarDialog"; +import { CustomizeSidebarSettings } from "./CustomizeSidebarDialog"; -function availability( - overrides: Partial> = {}, -) { - return { - ...(Object.fromEntries( - CUSTOMIZABLE_NAV_ITEM_IDS.map((id) => [id, true]), - ) as Record), - ...overrides, - }; -} - -function renderDialog(available = availability()) { +function renderSettings() { return render( - + , ); } @@ -107,7 +88,7 @@ function rowLabels() { .map((checkbox) => checkbox.closest("label")?.textContent); } -describe("CustomizeSidebarDialog", () => { +describe("CustomizeSidebarSettings", () => { beforeEach(() => { track.mockReset(); useSidebarStore.setState({ navItemOverrides: {}, navItemOrder: [] }); @@ -115,7 +96,7 @@ describe("CustomizeSidebarDialog", () => { it("unchecking a visible item demotes it and tracks the change", async () => { const user = userEvent.setup(); - renderDialog(); + renderSettings(); await user.click(screen.getByRole("checkbox", { name: "Command Center" })); @@ -130,40 +111,30 @@ describe("CustomizeSidebarDialog", () => { it("checking a hidden item promotes it and tracks the change", async () => { const user = userEvent.setup(); - renderDialog(); + useSidebarStore.setState({ navItemOverrides: { inbox: false } }); + renderSettings(); - await user.click(screen.getByRole("checkbox", { name: "Search" })); + await user.click(screen.getByRole("checkbox", { name: "Inbox" })); - expect(useSidebarStore.getState().navItemOverrides.search).toBe(true); + expect(useSidebarStore.getState().navItemOverrides.inbox).toBe(true); expect(track).toHaveBeenCalledWith(ANALYTICS_EVENTS.SIDEBAR_CUSTOMIZED, { - item: "search", + item: "inbox", visible: true, }); }); - it("omits items marked unavailable", () => { - renderDialog(availability({ loops: false })); - - expect( - screen.queryByRole("checkbox", { name: "Loops" }), - ).not.toBeInTheDocument(); - expect( - screen.getByRole("checkbox", { name: "Configure" }), - ).toBeInTheDocument(); - }); - it("renders rows in the stored order", () => { - useSidebarStore.setState({ navItemOrder: ["configure", "search"] }); - renderDialog(); + useSidebarStore.setState({ navItemOrder: ["configure", "inbox"] }); + renderSettings(); - expect(rowLabels().slice(0, 2)).toEqual(["Configure", "Search"]); + expect(rowLabels().slice(0, 2)).toEqual(["Configure", "Inbox"]); }); it("previews on dragover and persists only on drop", () => { - renderDialog(); + renderSettings(); dragStart("loops"); - dragOver("loops", "search"); + dragOver("loops", "inbox"); expect(rowLabels()[0]).toBe("Loops"); expect(useSidebarStore.getState().navItemOrder).toEqual([]); @@ -173,7 +144,6 @@ describe("CustomizeSidebarDialog", () => { expect(useSidebarStore.getState().navItemOrder).toEqual([ "loops", - "search", "inbox", "command-center", "contexts", @@ -187,11 +157,11 @@ describe("CustomizeSidebarDialog", () => { }); it("ignores a repeated dragover for the same source and target", () => { - renderDialog(); + renderSettings(); dragStart("loops"); - dragOver("loops", "search"); - dragOver("loops", "search"); + dragOver("loops", "inbox"); + dragOver("loops", "inbox"); expect(rowLabels()[0]).toBe("Loops"); @@ -201,19 +171,19 @@ describe("CustomizeSidebarDialog", () => { }); it("a canceled drag drops the preview and leaves the store untouched", () => { - renderDialog(); + renderSettings(); dragStart("loops"); - dragOver("loops", "search"); + dragOver("loops", "inbox"); dragEnd("loops", { cancel: true }); - expect(rowLabels()[0]).toBe("Search"); + expect(rowLabels()[0]).toBe("Inbox"); expect(useSidebarStore.getState().navItemOrder).toEqual([]); expect(track).not.toHaveBeenCalled(); }); it("a drop without movement neither persists nor tracks", () => { - renderDialog(); + renderSettings(); dragStart("loops"); dragEnd("loops"); @@ -224,12 +194,12 @@ describe("CustomizeSidebarDialog", () => { it("reset clears the stored order back to the default", async () => { const user = userEvent.setup(); - useSidebarStore.setState({ navItemOrder: ["loops", "search"] }); - renderDialog(); + useSidebarStore.setState({ navItemOrder: ["loops", "inbox"] }); + renderSettings(); await user.click(screen.getByRole("button", { name: "Reset" })); expect(useSidebarStore.getState().navItemOrder).toEqual([]); - expect(rowLabels()[0]).toBe("Search"); + expect(rowLabels()[0]).toBe("Inbox"); }); }); diff --git a/packages/ui/src/features/sidebar/components/CustomizeSidebarDialog.tsx b/packages/ui/src/features/sidebar/components/CustomizeSidebarDialog.tsx index 3be20438c5..8b241d81ab 100644 --- a/packages/ui/src/features/sidebar/components/CustomizeSidebarDialog.tsx +++ b/packages/ui/src/features/sidebar/components/CustomizeSidebarDialog.tsx @@ -6,7 +6,6 @@ import { EnvelopeSimple, HashIcon, Lightning, - MagnifyingGlass, RepeatIcon, SlidersHorizontal, } from "@phosphor-icons/react"; @@ -21,14 +20,13 @@ import { } from "@posthog/ui/features/sidebar/constants"; import { useSidebarStore } from "@posthog/ui/features/sidebar/sidebarStore"; import { track } from "@posthog/ui/shell/analytics"; -import { Button, Checkbox, Dialog, Flex, Text } from "@radix-ui/themes"; +import { Button, Checkbox, Flex, Text } from "@radix-ui/themes"; import { type RefCallback, useRef, useState } from "react"; const ITEM_ICONS: Record< CustomizableNavItemId, React.ComponentType<{ size?: number | string }> > = { - search: MagnifyingGlass, inbox: EnvelopeSimple, "command-center": Lightning, contexts: HashIcon, @@ -44,19 +42,7 @@ function sameOrder( return a.length === b.length && a.every((id, i) => id === b[i]); } -interface CustomizeSidebarDialogProps { - open: boolean; - onOpenChange: (open: boolean) => void; - // Items gated off by feature flags stay out of the dialog too, so it never - // offers a checkbox for a nav row the user can't have. - available?: Record; -} - -export function CustomizeSidebarDialog({ - open, - onOpenChange, - available, -}: CustomizeSidebarDialogProps) { +export function CustomizeSidebarSettings() { const navItemOverrides = useSidebarStore((s) => s.navItemOverrides); const navItemOrder = useSidebarStore((s) => s.navItemOrder); const setNavItemVisible = useSidebarStore((s) => s.setNavItemVisible); @@ -78,9 +64,7 @@ export function CustomizeSidebarDialog({ // sits on a row boundary; replaying the move would swap the rows back. const lastMove = useRef(null); - const items = orderedNavItems(previewOrder ?? navItemOrder).filter( - ({ id }) => available?.[id] !== false, - ); + const items = orderedNavItems(previewOrder ?? navItemOrder); const handleDragStart: DragDropEvents["dragstart"] = () => { lastMove.current = null; @@ -125,57 +109,48 @@ export function CustomizeSidebarDialog({ }; return ( - - - Customize sidebar - - Choose which items appear in your sidebar and drag to reorder. - Unchecked items live under More. - + + + Choose which items appear in your sidebar and drag to reorder. + - {/* Default pointer activation starts a mouse drag from the handle + {/* Default pointer activation starts a mouse drag from the handle immediately; a distance constraint here would delay pickup. */} - - - {items.map((item, index) => ( - { - setNavItemVisible(item.id, nextVisible); - track(ANALYTICS_EVENTS.SIDEBAR_CUSTOMIZED, { - item: item.analyticsId, - visible: nextVisible, - }); - }} - /> - ))} - - - - - - - - + + + {items.map((item, index) => ( + { + setNavItemVisible(item.id, nextVisible); + track(ANALYTICS_EVENTS.SIDEBAR_CUSTOMIZED, { + item: item.analyticsId, + visible: nextVisible, + }); + }} + /> + ))} - - + + + + + + ); } diff --git a/packages/ui/src/features/sidebar/components/SidebarNavSection.test.tsx b/packages/ui/src/features/sidebar/components/SidebarNavSection.test.tsx index 2172453dc5..a22a6a28fb 100644 --- a/packages/ui/src/features/sidebar/components/SidebarNavSection.test.tsx +++ b/packages/ui/src/features/sidebar/components/SidebarNavSection.test.tsx @@ -103,47 +103,28 @@ describe("SidebarNavSection", () => { }); }); + it("renders Search directly and removes the More dropdown", () => { + renderNav(); + + expect(screen.getByRole("button", { name: /Search/ })).toBeInTheDocument(); + expect( + screen.queryByRole("button", { name: "More" }), + ).not.toBeInTheDocument(); + }); + it.each([ - ["search", "Search"], ["inbox", "Inbox"], ["command-center", "Command Center"], ["contexts", "Channels"], ["activity", "Activity"], ["configure", "Configure"], ["loops", "Loops"], - ] as const)( - "moves %s from the top level into More when hidden", - async (id, label) => { - const user = userEvent.setup(); - useSidebarStore.setState({ navItemOverrides: { [id]: false } }); - renderNav(); - - expect(screen.queryByText(label)).not.toBeInTheDocument(); - - await user.click(screen.getByRole("button", { name: "More" })); - - expect(screen.getByText(label)).toBeInTheDocument(); - }, - ); - - it.each([ - ["inbox", "inbox", "Inbox"], - ["command-center", "command-center", "Command Center"], - ["activity", "activity", "Activity"], - ["loops", "loops", "Loops"], - ] as const)( - "active hidden %s takes over the collapsed More row", - (id, viewType, label) => { - useAppView.mockReturnValue({ type: viewType }); - useSidebarStore.setState({ navItemOverrides: { [id]: false } }); - renderNav(); + ] as const)("removes %s from the sidebar when hidden", (id, label) => { + useSidebarStore.setState({ navItemOverrides: { [id]: false } }); + renderNav(); - expect( - screen.queryByRole("button", { name: "More" }), - ).not.toBeInTheDocument(); - expect(screen.getByRole("button", { name: label })).toBeInTheDocument(); - }, - ); + expect(screen.queryByText(label)).not.toBeInTheDocument(); + }); it("renders top-level items in the stored order", () => { useSidebarStore.setState({ navItemOrder: ["activity", "inbox"] }); @@ -159,13 +140,6 @@ describe("SidebarNavSection", () => { expect(position("Inbox")).toBeLessThan(position("Loops")); }); - it("never lets hidden search take over the More row", () => { - useSidebarStore.setState({ navItemOverrides: { search: false } }); - renderNav(); - - expect(screen.getByRole("button", { name: "More" })).toBeInTheDocument(); - }); - it("tracks top-level clicks with in_more false", async () => { const user = userEvent.setup(); renderNav(); @@ -179,21 +153,6 @@ describe("SidebarNavSection", () => { ); }); - it("tracks clicks inside the expanded More section with in_more true", async () => { - const user = userEvent.setup(); - useSidebarStore.setState({ navItemOverrides: { inbox: false } }); - renderNav(); - - await user.click(screen.getByRole("button", { name: "More" })); - await user.click(screen.getByRole("button", { name: /Inbox/ })); - - expect(navigateToInbox).toHaveBeenCalledTimes(1); - expect(track).toHaveBeenCalledWith( - ANALYTICS_EVENTS.SIDEBAR_NAV_ITEM_CLICKED, - { item: "inbox", in_more: true }, - ); - }); - it.each([ [false, true, "enter_space"], [true, false, "leave_space"], diff --git a/packages/ui/src/features/sidebar/components/SidebarNavSection.tsx b/packages/ui/src/features/sidebar/components/SidebarNavSection.tsx index e8feebe566..963b14e85c 100644 --- a/packages/ui/src/features/sidebar/components/SidebarNavSection.tsx +++ b/packages/ui/src/features/sidebar/components/SidebarNavSection.tsx @@ -31,17 +31,14 @@ import { track } from "@posthog/ui/shell/analytics"; import { useCommandMenuStore } from "@posthog/ui/shell/commandMenuStore"; import { Box, Flex } from "@radix-ui/themes"; import { useRouterState } from "@tanstack/react-router"; -import { Fragment, type ReactNode, useState } from "react"; -import { CustomizeSidebarDialog } from "./CustomizeSidebarDialog"; +import type { ReactNode } from "react"; import { ActivityItem } from "./items/ActivityItem"; import { CommandCenterItem } from "./items/CommandCenterItem"; import { ConfigureItem } from "./items/ConfigureItem"; import { ContextsItem } from "./items/ContextsItem"; -import { CustomizeSidebarItem } from "./items/CustomizeSidebarItem"; import { HomeItem } from "./items/HomeItem"; import { InboxItem } from "./items/InboxItem"; import { LoopsItem } from "./items/LoopsItem"; -import { MoreItem } from "./items/MoreItem"; import { NewTaskItem } from "./items/NewTaskItem"; import { SearchItem } from "./items/SearchItem"; @@ -159,24 +156,7 @@ export function SidebarNavSection({ (id) => !isNavItemVisible(navItemOverrides, id), ), ); - const [moreExpanded, setMoreExpanded] = useState(false); - const [customizeOpen, setCustomizeOpen] = useState(false); - - // While More is collapsed, an active item hidden under it takes over the - // More row so the current page stays visible. Search, Contexts and Configure - // never do: none of them is a routed page. - const moreItemActive: Record = { - search: false, - inbox: isInboxActive, - "command-center": isCommandCenterActive, - contexts: false, - activity: isActivityActive, - configure: false, - loops: isLoopsActive, - }; - const navItemAvailable: Record = { - search: true, inbox: true, "command-center": true, contexts: bluebirdEnabled, @@ -187,12 +167,6 @@ export function SidebarNavSection({ loops: loopsEnabled, }; - const activeHiddenItem = orderedItems.find( - ({ id }) => navItemAvailable[id] && hidden.has(id) && moreItemActive[id], - ); - const takeoverLabel = - !moreExpanded && activeHiddenItem ? activeHiddenItem.label : null; - const handleChannelsToggle = (depth: 0 | 1) => (checked: boolean) => { setChannelsEnabled(checked); track(ANALYTICS_EVENTS.SIDEBAR_NAV_ITEM_CLICKED, { @@ -217,12 +191,6 @@ export function SidebarNavSection({ CustomizableNavItemId, (depth: 0 | 1) => ReactNode > = { - search: (depth) => ( - - ), inbox: (depth) => ( navItemAvailable[id] && !hidden.has(id), ); - const moreItems = orderedItems.filter( - ({ id }) => navItemAvailable[id] && hidden.has(id), - ); - return ( @@ -293,41 +257,13 @@ export function SidebarNavSection({ )} + + + + {topLevelItems.map(({ id }) => ( {renderNavItem[id](0)} ))} - - {/* Hidden items plus the Customize entry live under More, always the - last row, like the app switcher pattern this mirrors. */} - - setMoreExpanded((e) => !e))} - /> - - {moreExpanded && ( - <> - {moreItems.map(({ id }) => ( - {renderNavItem[id](1)} - ))} - setCustomizeOpen(true), - 1, - )} - /> - - )} - - - ); } diff --git a/packages/ui/src/features/sidebar/components/items/CustomizeSidebarItem.tsx b/packages/ui/src/features/sidebar/components/items/CustomizeSidebarItem.tsx deleted file mode 100644 index 4a72d02b38..0000000000 --- a/packages/ui/src/features/sidebar/components/items/CustomizeSidebarItem.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { SlidersHorizontal } from "@phosphor-icons/react"; -import { SidebarItem } from "../SidebarItem"; - -interface CustomizeSidebarItemProps { - onClick: () => void; - depth?: number; -} - -export function CustomizeSidebarItem({ - onClick, - depth = 0, -}: CustomizeSidebarItemProps) { - return ( - } - label="Customize sidebar" - onClick={onClick} - /> - ); -} diff --git a/packages/ui/src/features/sidebar/components/items/MoreItem.tsx b/packages/ui/src/features/sidebar/components/items/MoreItem.tsx deleted file mode 100644 index 56a085c3d4..0000000000 --- a/packages/ui/src/features/sidebar/components/items/MoreItem.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { CaretDown } from "@phosphor-icons/react"; -import { cn } from "@posthog/quill"; -import { SidebarItem } from "../SidebarItem"; - -interface MoreItemProps { - expanded: boolean; - // Label of the active hidden item, shown in place of "More" while - // collapsed so the current page stays visible in the nav. - activeItemLabel?: string | null; - onClick: () => void; -} - -export function MoreItem({ - expanded, - activeItemLabel, - onClick, -}: MoreItemProps) { - return ( - - } - label={activeItemLabel ?? "More"} - isActive={Boolean(activeItemLabel)} - onClick={onClick} - /> - ); -} diff --git a/packages/ui/src/features/sidebar/constants.test.ts b/packages/ui/src/features/sidebar/constants.test.ts index 23f2656f3f..82b3815ceb 100644 --- a/packages/ui/src/features/sidebar/constants.test.ts +++ b/packages/ui/src/features/sidebar/constants.test.ts @@ -12,6 +12,10 @@ describe("CUSTOMIZABLE_NAV_ITEM_IDS", () => { expect.arrayContaining(["agents", "skills", "mcp-servers"]), ); }); + + it("keeps fixed search out of the customizable navigation", () => { + expect(CUSTOMIZABLE_NAV_ITEM_IDS).not.toContain("search"); + }); }); describe("orderedNavItems", () => { @@ -32,18 +36,20 @@ describe("orderedNavItems", () => { }); it("inserts a missing id with no present predecessor at the start", () => { - const ids = orderedNavItems(["loops", "inbox"]).map((item) => item.id); + const ids = orderedNavItems(["command-center", "loops"]).map( + (item) => item.id, + ); - expect(ids[0]).toBe("search"); + expect(ids[0]).toBe("inbox"); }); it("puts stored ids first and appends the rest in default order", () => { - const ids = orderedNavItems(["configure", "search"]).map((item) => item.id); + const ids = orderedNavItems(["configure", "inbox"]).map((item) => item.id); - expect(ids.slice(0, 2)).toEqual(["configure", "search"]); + expect(ids.slice(0, 2)).toEqual(["configure", "inbox"]); expect(ids.slice(2)).toEqual( CUSTOMIZABLE_NAV_ITEM_IDS.filter( - (id) => id !== "configure" && id !== "search", + (id) => id !== "configure" && id !== "inbox", ), ); }); @@ -51,26 +57,26 @@ describe("orderedNavItems", () => { describe("moveNavItem", () => { it("moves an item backward to the target position", () => { - const next = moveNavItem([], "loops", "search"); + const next = moveNavItem([], "loops", "inbox"); expect(next[0]).toBe("loops"); expect(next).toHaveLength(CUSTOMIZABLE_NAV_ITEM_IDS.length); }); it("moves an item forward to the target position", () => { - const next = moveNavItem([], "search", "loops"); + const next = moveNavItem([], "inbox", "loops"); - expect(next.indexOf("search")).toBe( + expect(next.indexOf("inbox")).toBe( CUSTOMIZABLE_NAV_ITEM_IDS.indexOf("loops"), ); }); it.each([ - ["an unknown source", "retired-item", "search"], - ["an unknown target", "search", "retired-item"], - ["the same source and target", "search", "search"], + ["an unknown source", "retired-item", "inbox"], + ["an unknown target", "inbox", "retired-item"], + ["the same source and target", "inbox", "inbox"], ])("returns the order unchanged for %s", (_label, source, target) => { - const order: readonly ("loops" | "search")[] = ["loops", "search"]; + const order: readonly ("loops" | "inbox")[] = ["loops", "inbox"]; expect(moveNavItem(order, source, target)).toBe(order); }); @@ -79,7 +85,7 @@ describe("moveNavItem", () => { describe("sanitizeNavItemOrder", () => { it.each([ ["a string", "corrupt"], - ["an object", { search: 0 }], + ["an object", { inbox: 0 }], ["null", null], ["a number", 7], ])("returns an empty order when the value is %s", (_label, value) => { @@ -88,7 +94,7 @@ describe("sanitizeNavItemOrder", () => { it("drops unknown ids, non-strings and duplicates", () => { expect( - sanitizeNavItemOrder(["loops", "retired-item", 7, "search", "loops"]), - ).toEqual(["loops", "search"]); + sanitizeNavItemOrder(["loops", "retired-item", 7, "inbox", "loops"]), + ).toEqual(["loops", "inbox"]); }); }); diff --git a/packages/ui/src/features/sidebar/constants.ts b/packages/ui/src/features/sidebar/constants.ts index 8011ae2429..ede8445df3 100644 --- a/packages/ui/src/features/sidebar/constants.ts +++ b/packages/ui/src/features/sidebar/constants.ts @@ -3,12 +3,6 @@ import type { SidebarNavItem } from "@posthog/shared/analytics-events"; export const SIDEBAR_MIN_WIDTH = 240; export const CUSTOMIZABLE_NAV_ITEMS = [ - { - id: "search", - label: "Search", - analyticsId: "search", - defaultVisible: false, - }, { id: "inbox", label: "Inbox", analyticsId: "inbox", defaultVisible: true }, { id: "loops", diff --git a/packages/ui/src/features/sidebar/sidebarStore.test.ts b/packages/ui/src/features/sidebar/sidebarStore.test.ts index b942b1e45e..fd243b6617 100644 --- a/packages/ui/src/features/sidebar/sidebarStore.test.ts +++ b/packages/ui/src/features/sidebar/sidebarStore.test.ts @@ -78,7 +78,7 @@ describe("sidebarStore navItemOverrides", () => { "sidebar-storage", JSON.stringify({ state: { - navItemOrder: ["loops", "retired-item", 7, "search", "loops"], + navItemOrder: ["loops", "retired-item", 7, "inbox", "loops"], }, version: 0, }), @@ -86,10 +86,7 @@ describe("sidebarStore navItemOverrides", () => { await useSidebarStore.persist.rehydrate(); - expect(useSidebarStore.getState().navItemOrder).toEqual([ - "loops", - "search", - ]); + expect(useSidebarStore.getState().navItemOrder).toEqual(["loops", "inbox"]); localStorage.removeItem("sidebar-storage"); }); @@ -111,7 +108,7 @@ describe("sidebarStore navItemOverrides", () => { JSON.stringify({ state: { navItemOverrides: { - search: true, + inbox: true, "retired-item": true, skills: "yes", }, @@ -123,7 +120,7 @@ describe("sidebarStore navItemOverrides", () => { await useSidebarStore.persist.rehydrate(); expect(useSidebarStore.getState().navItemOverrides).toEqual({ - search: true, + inbox: true, }); localStorage.removeItem("sidebar-storage"); }); From af048dfb54884aa70f3e099356e985c60e628f32 Mon Sep 17 00:00:00 2001 From: Peter Kirkham Date: Wed, 22 Jul 2026 10:28:50 +0200 Subject: [PATCH 3/3] Hide unavailable sidebar settings Generated-By: PostHog Code Task-Id: 884159da-252c-4037-ab49-c6178d27732e --- .../components/CustomizeSidebarDialog.test.tsx | 18 +++++++++++++++++- .../components/CustomizeSidebarDialog.tsx | 17 ++++++++++++++++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/features/sidebar/components/CustomizeSidebarDialog.test.tsx b/packages/ui/src/features/sidebar/components/CustomizeSidebarDialog.test.tsx index a57bfb3280..de9c7ada6a 100644 --- a/packages/ui/src/features/sidebar/components/CustomizeSidebarDialog.test.tsx +++ b/packages/ui/src/features/sidebar/components/CustomizeSidebarDialog.test.tsx @@ -1,3 +1,4 @@ +import { LOOPS_FLAG } from "@posthog/shared"; import { ANALYTICS_EVENTS } from "@posthog/shared/analytics-events"; import { Theme } from "@radix-ui/themes"; import { act, render, screen } from "@testing-library/react"; @@ -9,8 +10,9 @@ type CapturedDragEvent = { canceled?: boolean; }; -const { track, dndCapture } = vi.hoisted(() => ({ +const { track, dndCapture, featureFlags } = vi.hoisted(() => ({ track: vi.fn(), + featureFlags: new Map(), dndCapture: {} as { onDragStart?: (event: CapturedDragEvent) => void; onDragOver?: (event: CapturedDragEvent) => void; @@ -19,6 +21,9 @@ const { track, dndCapture } = vi.hoisted(() => ({ })); vi.mock("@posthog/ui/shell/analytics", () => ({ track })); +vi.mock("@posthog/ui/features/feature-flags/useFeatureFlag", () => ({ + useFeatureFlag: (key: string) => featureFlags.get(key) ?? true, +})); vi.mock("@dnd-kit/react", () => ({ DragDropProvider: ({ onDragStart, @@ -91,9 +96,20 @@ function rowLabels() { describe("CustomizeSidebarSettings", () => { beforeEach(() => { track.mockReset(); + featureFlags.clear(); useSidebarStore.setState({ navItemOverrides: {}, navItemOrder: [] }); }); + it("omits items whose features are unavailable", () => { + featureFlags.set(LOOPS_FLAG, false); + + renderSettings(); + + expect( + screen.queryByRole("checkbox", { name: "Loops" }), + ).not.toBeInTheDocument(); + }); + it("unchecking a visible item demotes it and tracks the change", async () => { const user = userEvent.setup(); renderSettings(); diff --git a/packages/ui/src/features/sidebar/components/CustomizeSidebarDialog.tsx b/packages/ui/src/features/sidebar/components/CustomizeSidebarDialog.tsx index 8b241d81ab..7aafac9bb3 100644 --- a/packages/ui/src/features/sidebar/components/CustomizeSidebarDialog.tsx +++ b/packages/ui/src/features/sidebar/components/CustomizeSidebarDialog.tsx @@ -9,7 +9,9 @@ import { RepeatIcon, SlidersHorizontal, } from "@phosphor-icons/react"; +import { LOOPS_FLAG, PROJECT_BLUEBIRD_FLAG } from "@posthog/shared"; import { ANALYTICS_EVENTS } from "@posthog/shared/analytics-events"; +import { useFeatureFlag } from "@posthog/ui/features/feature-flags/useFeatureFlag"; import { CUSTOMIZABLE_NAV_ITEMS, type CustomizableNavItem, @@ -43,6 +45,12 @@ function sameOrder( } export function CustomizeSidebarSettings() { + const loopsEnabled = useFeatureFlag(LOOPS_FLAG, import.meta.env.DEV); + const bluebirdEnabled = useFeatureFlag( + PROJECT_BLUEBIRD_FLAG, + import.meta.env.DEV, + ); + const channelsEnabled = useSidebarStore((s) => s.channelsEnabled); const navItemOverrides = useSidebarStore((s) => s.navItemOverrides); const navItemOrder = useSidebarStore((s) => s.navItemOrder); const setNavItemVisible = useSidebarStore((s) => s.setNavItemVisible); @@ -64,7 +72,14 @@ export function CustomizeSidebarSettings() { // sits on a row boundary; replaying the move would swap the rows back. const lastMove = useRef(null); - const items = orderedNavItems(previewOrder ?? navItemOrder); + const items = orderedNavItems(previewOrder ?? navItemOrder).filter( + ({ id }) => { + if (id === "loops") return loopsEnabled; + if (id === "contexts") return bluebirdEnabled; + if (id === "activity") return bluebirdEnabled && channelsEnabled; + return true; + }, + ); const handleDragStart: DragDropEvents["dragstart"] = () => { lastMove.current = null;