From 265811c2cc3a71e4264e2f71909c4aa6db5c8069 Mon Sep 17 00:00:00 2001 From: Peter Kirkham Date: Sun, 26 Jul 2026 06:28:38 +0100 Subject: [PATCH 1/6] Move Channels switch into task panel header Generated-By: PostHog Code Task-Id: 0c011a69-42cd-47b7-ad39-634de03b9cb8 --- .../canvas/components/ChannelsSidebar.tsx | 13 ++-- .../CustomizeSidebarDialog.test.tsx | 1 - .../components/CustomizeSidebarDialog.tsx | 3 - .../sidebar/components/SidebarMenu.tsx | 3 - .../components/SidebarNavSection.test.tsx | 34 ++-------- .../sidebar/components/SidebarNavSection.tsx | 31 +-------- .../sidebar/components/TasksHeader.test.tsx | 63 +++++++++++++++++++ .../sidebar/components/TasksHeader.tsx | 50 +++++++++++++-- .../sidebar/components/items/ContextsItem.tsx | 38 ----------- packages/ui/src/features/sidebar/constants.ts | 6 -- 10 files changed, 120 insertions(+), 122 deletions(-) create mode 100644 packages/ui/src/features/sidebar/components/TasksHeader.test.tsx delete mode 100644 packages/ui/src/features/sidebar/components/items/ContextsItem.tsx diff --git a/packages/ui/src/features/canvas/components/ChannelsSidebar.tsx b/packages/ui/src/features/canvas/components/ChannelsSidebar.tsx index 3afc553050..92b7caca39 100644 --- a/packages/ui/src/features/canvas/components/ChannelsSidebar.tsx +++ b/packages/ui/src/features/canvas/components/ChannelsSidebar.tsx @@ -11,6 +11,7 @@ import { useOnboardingStore } from "@posthog/ui/features/onboarding/onboardingSt import { ProjectSwitcher } from "@posthog/ui/features/sidebar/components/ProjectSwitcher"; import { SidebarMenu } from "@posthog/ui/features/sidebar/components/SidebarMenu"; import { SidebarNavSection } from "@posthog/ui/features/sidebar/components/SidebarNavSection"; +import { TasksHeader } from "@posthog/ui/features/sidebar/components/TasksHeader"; import { UpdateBanner } from "@posthog/ui/features/sidebar/components/UpdateBanner"; import { beginSidebarPeek, @@ -27,9 +28,8 @@ import { Box, Flex } from "@radix-ui/themes"; import { useDeferredValue, useEffect } from "react"; // The unified app sidebar (Code merged into the Bluebird chrome). Top to -// bottom: workspace switcher, the merged global nav, the "Enable channels" -// opt-in, then the body — the task list by default, swapped for the channel -// tree once channels are enabled — and Settings pinned to the bottom. +// bottom: the merged global nav, the Tasks/Channels body header, then the +// selected list and the workspace switcher pinned to the bottom. export function ChannelsSidebar() { const width = useChannelsSidebarStore((state) => state.width); const setWidth = useChannelsSidebarStore((state) => state.setWidth); @@ -69,7 +69,7 @@ export function ChannelsSidebar() { // while peeked (route without it), a stale peek would greet the remount. useEffect(() => () => cancelSidebarPeek(), []); - // Channels stay behind project-bluebird: the toggle only appears where the + // Channels stay behind project-bluebird: the switch only appears where the // canvas backend is wired, and a persisted "on" is ignored when the flag is // off so the sidebar can't strand a user on an unsupported feature. const bluebirdEnabled = useFeatureFlag( @@ -78,7 +78,7 @@ export function ChannelsSidebar() { ); const channelsEnabled = useSidebarStore((s) => s.channelsEnabled) && bluebirdEnabled; - // The Switch (in SidebarNavSection) reads the live value and flips instantly. + // The Switch in TasksHeader reads the live value and flips instantly. // Swapping the sidebar body mounts a heavy tree (ChannelsList: the channels // query + a provider-laden row per channel), so defer that decision: the // urgent commit keeps the current body and paints the toggle, then the tree @@ -102,9 +102,8 @@ export function ChannelsSidebar() { onPeekDismiss={cancelSidebarPeek} > - {/* The nav owns the "Enable channels" toggle + Canvas rows (gated by - the same flag), so this section carries the whole merged nav. */} + {/* Body: the channel tree when channels are on, otherwise the task list. Each owns its own scroll region. Gated on the deferred value so diff --git a/packages/ui/src/features/sidebar/components/CustomizeSidebarDialog.test.tsx b/packages/ui/src/features/sidebar/components/CustomizeSidebarDialog.test.tsx index de9c7ada6a..8ad10195e8 100644 --- a/packages/ui/src/features/sidebar/components/CustomizeSidebarDialog.test.tsx +++ b/packages/ui/src/features/sidebar/components/CustomizeSidebarDialog.test.tsx @@ -162,7 +162,6 @@ describe("CustomizeSidebarSettings", () => { "loops", "inbox", "command-center", - "contexts", "activity", "configure", ]); diff --git a/packages/ui/src/features/sidebar/components/CustomizeSidebarDialog.tsx b/packages/ui/src/features/sidebar/components/CustomizeSidebarDialog.tsx index 7aafac9bb3..2fb62a41e8 100644 --- a/packages/ui/src/features/sidebar/components/CustomizeSidebarDialog.tsx +++ b/packages/ui/src/features/sidebar/components/CustomizeSidebarDialog.tsx @@ -4,7 +4,6 @@ import { Bell, DotsSixVertical, EnvelopeSimple, - HashIcon, Lightning, RepeatIcon, SlidersHorizontal, @@ -31,7 +30,6 @@ const ITEM_ICONS: Record< > = { inbox: EnvelopeSimple, "command-center": Lightning, - contexts: HashIcon, activity: Bell, configure: SlidersHorizontal, loops: RepeatIcon, @@ -75,7 +73,6 @@ export function CustomizeSidebarSettings() { 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; }, diff --git a/packages/ui/src/features/sidebar/components/SidebarMenu.tsx b/packages/ui/src/features/sidebar/components/SidebarMenu.tsx index 3be10a1f11..d915f3ccda 100644 --- a/packages/ui/src/features/sidebar/components/SidebarMenu.tsx +++ b/packages/ui/src/features/sidebar/components/SidebarMenu.tsx @@ -36,7 +36,6 @@ import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { ArchiveRunningTaskDialog } from "./ArchiveRunningTaskDialog"; import { SidebarItem } from "./SidebarItem"; import { TaskListView } from "./TaskListView"; -import { TasksHeader } from "./TasksHeader"; const log = logger.scope("sidebar-menu"); @@ -484,8 +483,6 @@ function SidebarMenuComponent() { id="side-bar-menu" className="flex min-h-0 flex-col" > - -
{sidebarData.isLoading ? ( diff --git a/packages/ui/src/features/sidebar/components/SidebarNavSection.test.tsx b/packages/ui/src/features/sidebar/components/SidebarNavSection.test.tsx index 0525255c57..025f4af101 100644 --- a/packages/ui/src/features/sidebar/components/SidebarNavSection.test.tsx +++ b/packages/ui/src/features/sidebar/components/SidebarNavSection.test.tsx @@ -113,7 +113,6 @@ describe("SidebarNavSection", () => { it.each([ ["inbox", "Inbox"], ["command-center", "Command Center"], - ["contexts", "Channels"], ["activity", "Activity"], ["configure", "Configure"], ["loops", "Loops"], @@ -151,31 +150,10 @@ describe("SidebarNavSection", () => { ); }); - it.each([ - [false, true, "enter_space"], - [true, false, "leave_space"], - ] as const)( - "toggling contexts from %s tracks the toggle and %s", - async (initial, expected, spaceAction) => { - const user = userEvent.setup(); - useSidebarStore.setState({ channelsEnabled: initial }); - renderNav(); - - await user.click(screen.getByRole("switch")); - - expect(useSidebarStore.getState().channelsEnabled).toBe(expected); - expect(track).toHaveBeenCalledWith( - ANALYTICS_EVENTS.SIDEBAR_NAV_ITEM_CLICKED, - { item: "contexts", in_more: false }, - ); - expect(track).toHaveBeenCalledWith(ANALYTICS_EVENTS.CHANNEL_ACTION, { - action_type: "toggle_channels", - surface: "nav", - }); - expect(track).toHaveBeenCalledWith(ANALYTICS_EVENTS.CHANNEL_ACTION, { - action_type: spaceAction, - surface: "nav", - }); - }, - ); + it("does not render the Channels mode toggle in navigation", () => { + renderNav(); + + expect(screen.queryByText("Channels")).not.toBeInTheDocument(); + expect(screen.queryByRole("switch")).not.toBeInTheDocument(); + }); }); diff --git a/packages/ui/src/features/sidebar/components/SidebarNavSection.tsx b/packages/ui/src/features/sidebar/components/SidebarNavSection.tsx index a9d3dd9200..40ce71efbf 100644 --- a/packages/ui/src/features/sidebar/components/SidebarNavSection.tsx +++ b/packages/ui/src/features/sidebar/components/SidebarNavSection.tsx @@ -32,7 +32,6 @@ 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 { InboxItem } from "./items/InboxItem"; import { LoopsItem } from "./items/LoopsItem"; import { NewTaskItem } from "./items/NewTaskItem"; @@ -65,15 +64,13 @@ export function SidebarNavSection({ // Loops stays behind the loops flag; default on in dev so local builds // keep the nav item. Also gates the per-channel Loops tab (see ChannelTabs). const loopsEnabled = useFeatureFlag(LOOPS_FLAG, import.meta.env.DEV); - // Channels stay behind project-bluebird: the "Enable channels" nav row (and - // the Canvas row it reveals) only appear where the canvas backend is wired. + // Channels stay behind project-bluebird, including channel-only nav items. const bluebirdEnabled = useFeatureFlag( PROJECT_BLUEBIRD_FLAG, import.meta.env.DEV, ); const channelsEnabled = useSidebarStore((s) => s.channelsEnabled) && bluebirdEnabled; - const setChannelsEnabled = useSidebarStore((s) => s.setChannelsEnabled); // When this section renders inside the Channels space, the destinations that // have a /website mirror stay in that space; everything else (and the whole @@ -152,7 +149,6 @@ export function SidebarNavSection({ const navItemAvailable: Record = { inbox: true, "command-center": true, - contexts: bluebirdEnabled, // Activity (the mentions feed) is a channels surface, so it only appears // once channels are enabled. activity: channelsEnabled, @@ -160,24 +156,6 @@ export function SidebarNavSection({ loops: loopsEnabled, }; - const handleChannelsToggle = (depth: 0 | 1) => (checked: boolean) => { - setChannelsEnabled(checked); - track(ANALYTICS_EVENTS.SIDEBAR_NAV_ITEM_CLICKED, { - item: "contexts", - in_more: depth === 1, - }); - track(ANALYTICS_EVENTS.CHANNEL_ACTION, { - action_type: "toggle_channels", - surface: "nav", - }); - // This toggle replaced the old Code/Channels space boundary; keep firing - // the legacy enter/leave events so space-adoption dashboards stay continuous. - track(ANALYTICS_EVENTS.CHANNEL_ACTION, { - action_type: checked ? "enter_space" : "leave_space", - surface: "nav", - }); - }; - // One renderer per customizable item, used for both the top level (depth 0) // and the expanded More section (depth 1) so the two never drift apart. const renderNavItem: Record< @@ -200,13 +178,6 @@ export function SidebarNavSection({ activeCount={commandCenterActiveCount} /> ), - contexts: (depth) => ( - - ), activity: (depth) => ( ({ track: vi.fn() })); + +vi.mock("@posthog/ui/shell/analytics", () => ({ track })); +vi.mock("@posthog/ui/features/feature-flags/useFeatureFlag", () => ({ + useFeatureFlag: () => true, +})); +vi.mock("@posthog/host-router/react", () => ({ + useHostTRPCClient: () => ({ os: { selectDirectory: { query: vi.fn() } } }), +})); +vi.mock("@posthog/ui/features/auth/useMeQuery", () => ({ + useMeQuery: () => ({ data: { is_staff: false } }), +})); +vi.mock("@posthog/ui/features/folders/useFolders", () => ({ + useFolders: () => ({ addFolder: vi.fn() }), +})); +vi.mock("@posthog/ui/features/sidebar/useHoldSidebarPeek", () => ({ + useHoldSidebarPeek: () => vi.fn(), +})); +vi.mock("@posthog/ui/shell/commandMenuStore", () => ({ + useCommandMenuStore: (selector: (state: { open: () => void }) => unknown) => + selector({ open: vi.fn() }), +})); + +import { useSidebarStore } from "@posthog/ui/features/sidebar/sidebarStore"; +import { TasksHeader } from "./TasksHeader"; + +describe("TasksHeader", () => { + beforeEach(() => { + vi.clearAllMocks(); + useSidebarStore.setState({ channelsEnabled: false }); + }); + + it("switches modes from the panel title and changes the available actions", async () => { + const user = userEvent.setup(); + render( + + + , + ); + + expect(screen.getByText("Tasks")).toBeInTheDocument(); + expect( + screen.getByRole("button", { name: "Filter tasks" }), + ).toBeInTheDocument(); + + await user.click(screen.getByRole("switch", { name: "Show channels" })); + + expect(screen.getByText("Channels")).toBeInTheDocument(); + expect( + screen.queryByRole("button", { name: "Filter tasks" }), + ).not.toBeInTheDocument(); + expect(track).toHaveBeenCalledWith(ANALYTICS_EVENTS.CHANNEL_ACTION, { + action_type: "toggle_channels", + surface: "nav", + }); + }); +}); diff --git a/packages/ui/src/features/sidebar/components/TasksHeader.tsx b/packages/ui/src/features/sidebar/components/TasksHeader.tsx index edb49e4e8e..8520e2b50f 100644 --- a/packages/ui/src/features/sidebar/components/TasksHeader.tsx +++ b/packages/ui/src/features/sidebar/components/TasksHeader.tsx @@ -22,14 +22,18 @@ import { DropdownMenuSubTrigger, DropdownMenuTrigger, MenuLabel, + Switch, } from "@posthog/quill"; -import type { WorkspaceMode } from "@posthog/shared"; +import { PROJECT_BLUEBIRD_FLAG, type WorkspaceMode } from "@posthog/shared"; +import { ANALYTICS_EVENTS } from "@posthog/shared/analytics-events"; import { useMeQuery } from "@posthog/ui/features/auth/useMeQuery"; +import { useFeatureFlag } from "@posthog/ui/features/feature-flags/useFeatureFlag"; import { useFolders } from "@posthog/ui/features/folders/useFolders"; import { useSidebarStore } from "@posthog/ui/features/sidebar/sidebarStore"; import { useHoldSidebarPeek } from "@posthog/ui/features/sidebar/useHoldSidebarPeek"; import { Tooltip } from "@posthog/ui/primitives/Tooltip"; import { toast } from "@posthog/ui/primitives/toast"; +import { track } from "@posthog/ui/shell/analytics"; import { useCommandMenuStore } from "@posthog/ui/shell/commandMenuStore"; import { logger } from "@posthog/ui/shell/logger"; import { useState } from "react"; @@ -214,15 +218,49 @@ function TaskFilterMenu() { } export function TasksHeader() { + const bluebirdEnabled = useFeatureFlag( + PROJECT_BLUEBIRD_FLAG, + import.meta.env.DEV, + ); + const channelsEnabled = + useSidebarStore((state) => state.channelsEnabled) && bluebirdEnabled; + const setChannelsEnabled = useSidebarStore( + (state) => state.setChannelsEnabled, + ); + + const handleModeChange = (checked: boolean) => { + setChannelsEnabled(checked); + track(ANALYTICS_EVENTS.CHANNEL_ACTION, { + action_type: "toggle_channels", + surface: "nav", + }); + track(ANALYTICS_EVENTS.CHANNEL_ACTION, { + action_type: checked ? "enter_space" : "leave_space", + surface: "nav", + }); + }; + return (
- Tasks - - - - + + {channelsEnabled ? "Channels" : "Tasks"} + {bluebirdEnabled && ( + + )} + {!channelsEnabled && ( + + + + + + )}
); diff --git a/packages/ui/src/features/sidebar/components/items/ContextsItem.tsx b/packages/ui/src/features/sidebar/components/items/ContextsItem.tsx deleted file mode 100644 index 547002d668..0000000000 --- a/packages/ui/src/features/sidebar/components/items/ContextsItem.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { HashIcon } from "@phosphor-icons/react"; -import { Badge, Switch } from "@posthog/quill"; -import { getSidebarItemPaddingLeft } from "../SidebarItem"; - -interface ContextsItemProps { - checked: boolean; - onCheckedChange: (checked: boolean) => void; - depth?: number; -} - -// A