diff --git a/packages/ui/src/features/canvas/components/WebsiteChannelHome.test.tsx b/packages/ui/src/features/canvas/components/WebsiteChannelHome.test.tsx new file mode 100644 index 0000000000..a8ccd8d356 --- /dev/null +++ b/packages/ui/src/features/canvas/components/WebsiteChannelHome.test.tsx @@ -0,0 +1,116 @@ +import { Theme } from "@radix-ui/themes"; +import { act, render, screen } from "@testing-library/react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +if (typeof globalThis.ResizeObserver === "undefined") { + globalThis.ResizeObserver = class { + observe() {} + unobserve() {} + disconnect() {} + } as unknown as typeof ResizeObserver; +} + +vi.mock("@posthog/ui/shell/rendererStorage", () => ({ + electronStorage: { + getItem: () => null, + setItem: () => {}, + removeItem: () => {}, + }, +})); +vi.mock("@posthog/ui/features/canvas/hooks/useChannels", () => ({ + useChannels: () => ({ + channels: [{ id: "chan-1", name: "eng" }], + isLoading: false, + }), +})); +vi.mock("@posthog/ui/features/canvas/hooks/useChannelsLayout", () => ({ + useChannelsLayout: () => true, +})); +vi.mock("@posthog/ui/features/canvas/hooks/useTaskChannels", () => ({ + PERSONAL_CHANNEL_NAME: "me", + useBackendChannel: () => ({ + channel: { id: "backend-1", name: "eng" }, + isLoading: false, + }), +})); +vi.mock("@posthog/ui/features/canvas/hooks/useChannelFeed", () => ({ + useChannelFeed: () => ({ tasks: [], isLoading: false }), + channelFeedQueryKey: () => ["feed"], +})); +vi.mock("@posthog/ui/features/canvas/hooks/useChannelFeedMessages", () => ({ + useChannelFeedMessages: () => ({ messages: [], isLoading: false }), + channelCreationMessage: () => null, +})); +vi.mock("@posthog/ui/features/canvas/hooks/useFolderInstructions", () => ({ + useFolderInstructions: () => ({ data: undefined, isLoading: false }), +})); +vi.mock("@posthog/ui/features/canvas/hooks/useChannelTasks", () => ({ + useChannelTaskMutations: () => ({ fileTask: () => Promise.resolve() }), +})); +vi.mock("@posthog/ui/hooks/useSetHeaderContent", () => ({ + useSetHeaderContent: () => {}, +})); +vi.mock("@posthog/ui/shell/analytics", () => ({ track: vi.fn() })); +vi.mock("@tanstack/react-query", () => ({ + useQueryClient: () => ({ setQueryData: vi.fn(), invalidateQueries: vi.fn() }), +})); +vi.mock("@tanstack/react-router", () => ({ useNavigate: () => vi.fn() })); + +// ThreadSidebar is the task dock under test; the rest of the channel chrome +// (feed rows, composer, intro) plays no part in the feed/sidebar exclusion. +vi.mock("@posthog/ui/features/canvas/components/ChannelFeedView", () => ({ + ChannelFeedView: () =>
, +})); +vi.mock("@posthog/ui/features/canvas/components/ChannelHomeComposer", () => ({ + ChannelHomeComposer: () => null, +})); +vi.mock("@posthog/ui/features/canvas/components/ChannelIntro", () => ({ + ChannelIntro: () => null, +})); +vi.mock("@posthog/ui/features/canvas/components/CreateChannelModal", () => ({ + CreateChannelModal: () => null, +})); +vi.mock("@posthog/ui/features/canvas/components/ThreadSidebar", () => ({ + ThreadSidebar: () =>
, +})); + +import { useThreadPanelStore } from "@posthog/ui/features/canvas/stores/threadPanelStore"; +import { WebsiteChannelHome } from "./WebsiteChannelHome"; + +describe("WebsiteChannelHome", () => { + beforeEach(() => { + useThreadPanelStore.setState({ + openByChannel: {}, + collapsed: false, + width: 360, + }); + }); + + it("drops a stale open thread so the feed can't show a task sidebar", () => { + useThreadPanelStore.getState().openThread("chan-1", "task-1"); + render( + + + , + ); + + expect(screen.getByTestId("feed")).toBeTruthy(); + expect(screen.queryByTestId("task-sidebar")).toBeNull(); + expect(useThreadPanelStore.getState().openByChannel["chan-1"]).toBeNull(); + }); + + it("shows the task sidebar for a thread opened from this feed", () => { + render( + + + , + ); + + act(() => { + useThreadPanelStore.getState().openThread("chan-1", "task-1"); + }); + + expect(screen.getByTestId("task-sidebar")).toBeTruthy(); + expect(screen.queryByTestId("feed")).toBeTruthy(); + }); +}); diff --git a/packages/ui/src/features/canvas/components/WebsiteChannelHome.tsx b/packages/ui/src/features/canvas/components/WebsiteChannelHome.tsx index 3195ba4877..e34e11a7d2 100644 --- a/packages/ui/src/features/canvas/components/WebsiteChannelHome.tsx +++ b/packages/ui/src/features/canvas/components/WebsiteChannelHome.tsx @@ -44,7 +44,7 @@ import { track } from "@posthog/ui/shell/analytics"; import { Heading, Text } from "@radix-ui/themes"; import { useQueryClient } from "@tanstack/react-query"; import { useNavigate } from "@tanstack/react-router"; -import { useCallback, useMemo, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; // A channel: a Slack-style multiplayer feed. Each member message kicks off a // task rendered as a card everyone in the channel sees; the composer stays @@ -135,6 +135,19 @@ export function WebsiteChannelHome({ channelId }: { channelId: string }) { const openThread = useThreadPanelStore((s) => s.openThread); const closeThread = useThreadPanelStore((s) => s.closeThread); + // The open thread outlives the thread view, so the feed showing itself is + // the only signal an inherited thread is gone. Suppress it in render (an + // effect alone would paint the sidebar for a frame first) and clear the + // store; threads opened from this feed instance paint normally. + const [inheritedThreadTaskId] = useState( + () => useThreadPanelStore.getState().openByChannel[channelId] ?? null, + ); + useEffect(() => { + if (inheritedThreadTaskId) { + useThreadPanelStore.getState().closeThread(channelId); + } + }, [channelId, inheritedThreadTaskId]); + const handleSuggestionSelect = useCallback( (prompt: string, mode?: string) => { composerRef.current?.applySuggestion(prompt, mode); @@ -317,7 +330,7 @@ export function WebsiteChannelHome({ channelId }: { channelId: string }) {
- {threadTaskId && ( + {threadTaskId && threadTaskId !== inheritedThreadTaskId && (