Skip to content
This repository was archived by the owner on Aug 6, 2026. It is now read-only.
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
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -69,6 +70,7 @@ const SETTINGS_PAGES: Record<SettingsCategory, SettingsPageDefinition> = {
"Personalization",
PersonalizationSettings,
),
sidebar: defineSettingsPage("Sidebar", CustomizeSidebarSettings),
terminal: defineSettingsPage("Terminal", TerminalSettings),
"claude-code": defineSettingsPage("Claude Code", ClaudeCodeSettings),
shortcuts: defineSettingsPage("Shortcuts", ShortcutsSettings),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
Palette,
Plugs,
Robot,
SidebarSimple,
SignOut,
SlackLogo,
Terminal,
Expand Down Expand Up @@ -101,6 +102,7 @@ const SIDEBAR_GROUPS: SidebarGroup[] = [
label: "Personalization",
icon: <Palette size={16} />,
},
{ id: "sidebar", label: "Sidebar", icon: <SidebarSimple size={16} /> },
{ id: "terminal", label: "Terminal", icon: <Terminal size={16} /> },
{ id: "shortcuts", label: "Shortcuts", icon: <Keyboard size={16} /> },
],
Expand Down
8 changes: 8 additions & 0 deletions packages/ui/src/features/settings/types.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
2 changes: 2 additions & 0 deletions packages/ui/src/features/settings/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export type SettingsCategory =
| "skills"
| "mcp-servers"
| "personalization"
| "sidebar"
| "terminal"
| "claude-code"
| "shortcuts"
Expand All @@ -32,6 +33,7 @@ export const SETTINGS_CATEGORIES: readonly SettingsCategory[] = [
"skills",
"mcp-servers",
"personalization",
"sidebar",
"terminal",
"claude-code",
"shortcuts",
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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<string, boolean>(),
dndCapture: {} as {
onDragStart?: (event: CapturedDragEvent) => void;
onDragOver?: (event: CapturedDragEvent) => void;
Expand All @@ -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,
Expand All @@ -45,32 +50,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<Record<CustomizableNavItemId, boolean>> = {},
) {
return {
...(Object.fromEntries(
CUSTOMIZABLE_NAV_ITEM_IDS.map((id) => [id, true]),
) as Record<CustomizableNavItemId, boolean>),
...overrides,
};
}

function renderDialog(available = availability()) {
function renderSettings() {
return render(
<Theme>
<CustomizeSidebarDialog
open
onOpenChange={vi.fn()}
available={available}
/>
<CustomizeSidebarSettings />
</Theme>,
);
}
Expand Down Expand Up @@ -107,132 +93,129 @@ function rowLabels() {
.map((checkbox) => checkbox.closest("label")?.textContent);
}

describe("CustomizeSidebarDialog", () => {
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();
renderDialog();
renderSettings();

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,
});
});

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("skills");
dragOver("skills", "search");
dragStart("loops");
dragOver("loops", "inbox");

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",
"search",
"inbox",
"agents",
"loops",
"mcp-servers",
"inbox",
"command-center",
"contexts",
"activity",
"configure",
]);
expect(track).toHaveBeenCalledWith(ANALYTICS_EVENTS.SIDEBAR_REORDERED, {
item: "skills",
item: "loops",
to_index: 0,
});
});

it("ignores a repeated dragover for the same source and target", () => {
renderDialog();
renderSettings();

dragStart("skills");
dragOver("skills", "search");
dragOver("skills", "search");
dragStart("loops");
dragOver("loops", "inbox");
dragOver("loops", "inbox");

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();
renderSettings();

dragStart("skills");
dragOver("skills", "search");
dragEnd("skills", { cancel: true });
dragStart("loops");
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("skills");
dragEnd("skills");
dragStart("loops");
dragEnd("loops");

expect(useSidebarStore.getState().navItemOrder).toEqual([]);
expect(track).not.toHaveBeenCalled();
});

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");
});
});
Loading
Loading