Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 6 additions & 7 deletions packages/ui/src/features/canvas/components/ChannelsSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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);
Expand Down Expand Up @@ -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(
Expand All @@ -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
Expand All @@ -102,9 +102,8 @@ export function ChannelsSidebar() {
onPeekDismiss={cancelSidebarPeek}
>
<Flex direction="column" className="h-full bg-chrome">
{/* The nav owns the "Enable channels" toggle + Canvas rows (gated by
the same flag), so this section carries the whole merged nav. */}
<SidebarNavSection />
<TasksHeader />

{/* Body: the channel tree when channels are on, otherwise the task
list. Each owns its own scroll region. Gated on the deferred value so
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { LOOPS_FLAG } from "@posthog/shared";
import { LOOPS_FLAG, PROJECT_BLUEBIRD_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";
Expand Down Expand Up @@ -110,6 +110,17 @@ describe("CustomizeSidebarSettings", () => {
).not.toBeInTheDocument();
});

it("keeps Activity customizable when Tasks mode is selected", () => {
featureFlags.set(PROJECT_BLUEBIRD_FLAG, true);
useSidebarStore.setState({ channelsEnabled: false });

renderSettings();

expect(
screen.getByRole("checkbox", { name: "Activity" }),
).toBeInTheDocument();
});

it("unchecking a visible item demotes it and tracks the change", async () => {
const user = userEvent.setup();
renderSettings();
Expand Down Expand Up @@ -161,9 +172,8 @@ describe("CustomizeSidebarSettings", () => {
expect(useSidebarStore.getState().navItemOrder).toEqual([
"loops",
"inbox",
"command-center",
"contexts",
"activity",
"command-center",
"configure",
]);
expect(track).toHaveBeenCalledWith(ANALYTICS_EVENTS.SIDEBAR_REORDERED, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
Bell,
DotsSixVertical,
EnvelopeSimple,
HashIcon,
Lightning,
RepeatIcon,
SlidersHorizontal,
Expand All @@ -31,7 +30,6 @@ const ITEM_ICONS: Record<
> = {
inbox: EnvelopeSimple,
"command-center": Lightning,
contexts: HashIcon,
activity: Bell,
configure: SlidersHorizontal,
loops: RepeatIcon,
Expand All @@ -50,7 +48,6 @@ export function CustomizeSidebarSettings() {
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);
Expand All @@ -75,8 +72,7 @@ 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;
if (id === "activity") return bluebirdEnabled;
return true;
},
);
Expand Down
3 changes: 0 additions & 3 deletions packages/ui/src/features/sidebar/components/SidebarMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down Expand Up @@ -484,8 +483,6 @@ function SidebarMenuComponent() {
id="side-bar-menu"
className="flex min-h-0 flex-col"
>
<TasksHeader />

<div className="min-h-0 flex-1 overflow-y-auto overflow-x-hidden">
<Flex direction="column" className="gap-px px-2 pb-2">
{sidebarData.isLoading ? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,6 @@ describe("SidebarNavSection", () => {
it.each([
["inbox", "Inbox"],
["command-center", "Command Center"],
["contexts", "Channels"],
["activity", "Activity"],
["configure", "Configure"],
["loops", "Loops"],
Expand All @@ -124,8 +123,7 @@ describe("SidebarNavSection", () => {
expect(screen.queryByText(label)).not.toBeInTheDocument();
});

it("renders top-level items in the stored order", () => {
useSidebarStore.setState({ navItemOrder: ["activity", "inbox"] });
it("renders Activity directly under Inbox by default", () => {
renderNav();

const labels = screen
Expand All @@ -134,7 +132,8 @@ describe("SidebarNavSection", () => {
const position = (label: string) =>
labels.findIndex((text) => text.includes(label));

expect(position("Activity")).toBeLessThan(position("Inbox"));
expect(position("Inbox")).toBeLessThan(position("Activity"));
expect(position("Activity")).toBeLessThan(position("Loops"));
expect(position("Inbox")).toBeLessThan(position("Loops"));
});

Expand All @@ -151,31 +150,18 @@ 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();
});

it("keeps Activity visible when Tasks mode is selected", () => {
useSidebarStore.setState({ channelsEnabled: false });

renderNav();

expect(screen.getByText("Activity")).toBeInTheDocument();
});
});
38 changes: 2 additions & 36 deletions packages/ui/src/features/sidebar/components/SidebarNavSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -65,16 +64,11 @@ 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
// section in the Code space) uses the canonical routes. Inbox and New task
Expand Down Expand Up @@ -152,32 +146,11 @@ export function SidebarNavSection({
const navItemAvailable: Record<CustomizableNavItemId, boolean> = {
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,
activity: bluebirdEnabled,
configure: true,
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<
Expand All @@ -200,13 +173,6 @@ export function SidebarNavSection({
activeCount={commandCenterActiveCount}
/>
),
contexts: (depth) => (
<ContextsItem
depth={depth}
checked={channelsEnabled}
onCheckedChange={handleChannelsToggle(depth)}
/>
),
activity: (depth) => (
<ActivityItem
depth={depth}
Expand Down
90 changes: 90 additions & 0 deletions packages/ui/src/features/sidebar/components/TasksHeader.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { ANALYTICS_EVENTS } from "@posthog/shared/analytics-events";
import { Theme } from "@radix-ui/themes";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { beforeEach, describe, expect, it, vi } from "vitest";

const { featureFlagEnabled, track } = vi.hoisted(() => ({
featureFlagEnabled: { value: true },
track: vi.fn(),
}));

vi.mock("@posthog/ui/shell/analytics", () => ({ track }));
vi.mock("@posthog/ui/features/feature-flags/useFeatureFlag", () => ({
useFeatureFlag: () => featureFlagEnabled.value,
}));
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();
featureFlagEnabled.value = true;
useSidebarStore.setState({ channelsEnabled: false });
});

it("switches modes from the panel title and changes the available actions", async () => {
const user = userEvent.setup();
render(
<Theme>
<TasksHeader />
</Theme>,
);

const listOption = screen.getByRole("button", { name: "List" });
const channelsOption = screen.getByRole("button", { name: "Channels" });
expect(listOption).toHaveAttribute("aria-pressed", "true");
expect(listOption).toHaveAttribute("data-active", "true");
expect(listOption).toHaveClass("data-[active]:bg-accent-4");
expect(listOption).toHaveClass("data-[active]:font-medium");
expect(listOption).toHaveClass("text-xs");
expect(channelsOption).toHaveClass("text-xs");
expect(channelsOption).not.toHaveAttribute("data-active");
expect(
screen.getByRole("button", { name: "Filter tasks" }),
).toBeInTheDocument();

await user.click(channelsOption);

expect(channelsOption).toHaveAttribute("aria-pressed", "true");
expect(channelsOption).toHaveAttribute("data-active", "true");
expect(listOption).not.toHaveAttribute("data-active");
expect(
screen.queryByRole("button", { name: "Filter tasks" }),
).not.toBeInTheDocument();
expect(track).toHaveBeenCalledWith(ANALYTICS_EVENTS.CHANNEL_ACTION, {
action_type: "toggle_channels",
surface: "nav",
});
});

it("hides the mode selector when Channels is unavailable", () => {
featureFlagEnabled.value = false;

render(
<Theme>
<TasksHeader />
</Theme>,
);

expect(screen.getByText("List")).toBeInTheDocument();
expect(screen.queryByText("Channels")).not.toBeInTheDocument();
});
});
Loading
Loading