Skip to content
Open
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
125 changes: 125 additions & 0 deletions packages/ui/src/features/canvas/components/WebsiteLayout.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { Theme } from "@radix-ui/themes";
import { render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";

const { useChannelTasks, useParams, usePathname, useTasks } = vi.hoisted(
() => ({
useChannelTasks: vi.fn(),
useParams: vi.fn(),
usePathname: vi.fn(),
useTasks: vi.fn(),
}),
);

vi.mock("@tanstack/react-router", () => ({
Outlet: () => null,
useNavigate: () => vi.fn(),
useParams,
useRouterState: ({
select,
}: {
select: (s: { location: { pathname: string } }) => string;
}) => select({ location: { pathname: usePathname() } }),
}));

vi.mock(
"@posthog/ui/features/task-detail/components/TaskHeaderActions",
() => ({
TaskHeaderActions: ({ task }: { task: { id: string } }) => (
<div data-testid="task-header-actions">{task.id}</div>
),
}),
);

vi.mock("@posthog/ui/features/tasks/useTasks", () => ({ useTasks }));
vi.mock("@posthog/ui/features/canvas/hooks/useChannelTasks", () => ({
useChannelTasks,
}));
vi.mock("@posthog/ui/features/canvas/hooks/useChannels", () => ({
useChannels: () => ({
channels: [{ id: "chan-1", name: "project-bluebird" }],
}),
}));
vi.mock("@posthog/ui/features/canvas/hooks/useDashboards", () => ({
useDashboard: () => ({ dashboard: undefined }),
useDashboardMutations: () => ({}),
}));
vi.mock("@posthog/ui/features/canvas/stores/dashboardEditStore", () => ({
useDashboardEditStore: (sel: (s: unknown) => unknown) =>
sel({ setEditing: vi.fn() }),
useIsDashboardEditing: () => false,
}));
vi.mock("@posthog/ui/features/canvas/stores/freeformChatStore", () => ({
useFreeformChatStore: (sel: (s: unknown) => unknown) =>
sel({ revert: vi.fn(), goToLatest: vi.fn() }),
useFreeformThread: () => ({
code: "",
versions: [],
currentVersionId: null,
isSaving: false,
}),
}));
vi.mock("@posthog/ui/features/canvas/components/NewCanvasMenu", () => ({
NewCanvasMenu: () => null,
}));
vi.mock("@posthog/ui/features/canvas/freeform/CanvasFrameHost", () => ({
CanvasFrameHost: () => null,
}));

import { useHeaderStore } from "@posthog/ui/shell/headerStore";
import { WebsiteLayout } from "./WebsiteLayout";

function renderLayout({
pathname,
params,
tasks = [{ id: "task-1", title: "Fix the bug" }],
channelTaskIds = tasks.map((task) => task.id),
}: {
pathname: string;
params: Record<string, string>;
tasks?: { id: string; title: string }[];
channelTaskIds?: string[];
}) {
usePathname.mockReturnValue(pathname);
useParams.mockReturnValue(params);
useTasks.mockReturnValue({ data: tasks });
useChannelTasks.mockReturnValue({
tasks: channelTaskIds.map((taskId) => ({ taskId })),
isLoading: false,
});
useHeaderStore.setState({ content: <span>crumb</span> });
render(
<Theme>
<WebsiteLayout />
</Theme>,
);
}

describe("WebsiteLayout task header actions", () => {
it("renders the task action row on a channel task detail", () => {
renderLayout({
pathname: "/website/chan-1/tasks/task-1",
params: { channelId: "chan-1", taskId: "task-1" },
});
expect(screen.getByTestId("task-header-actions")).toHaveTextContent(
"task-1",
);
});

it("does not render actions for a task filed to another channel", () => {
renderLayout({
pathname: "/website/chan-1/tasks/task-1",
params: { channelId: "chan-1", taskId: "task-1" },
channelTaskIds: ["other-task"],
});
expect(screen.queryByTestId("task-header-actions")).not.toBeInTheDocument();
});

it.each([
["channel home", "/website/chan-1", { channelId: "chan-1" }],
["new task", "/website/chan-1/new", { channelId: "chan-1" }],
])("does not render the action row on %s", (_label, pathname, params) => {
renderLayout({ pathname, params });
expect(screen.queryByTestId("task-header-actions")).not.toBeInTheDocument();
});
});
19 changes: 18 additions & 1 deletion packages/ui/src/features/canvas/components/WebsiteLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { iconForTemplate } from "@posthog/ui/features/canvas/components/canvasTe
import { NewCanvasMenu } from "@posthog/ui/features/canvas/components/NewCanvasMenu";
import { CanvasFrameHost } from "@posthog/ui/features/canvas/freeform/CanvasFrameHost";
import { useChannels } from "@posthog/ui/features/canvas/hooks/useChannels";
import { useChannelTasks } from "@posthog/ui/features/canvas/hooks/useChannelTasks";
import {
useDashboard,
useDashboardMutations,
Expand All @@ -32,6 +33,8 @@ import {
useFreeformThread,
} from "@posthog/ui/features/canvas/stores/freeformChatStore";
import { copyCanvasLink } from "@posthog/ui/features/canvas/utils/copyCanvasLink";
import { TaskHeaderActions } from "@posthog/ui/features/task-detail/components/TaskHeaderActions";
import { useTasks } from "@posthog/ui/features/tasks/useTasks";
import { toast } from "@posthog/ui/primitives/toast";
import { track } from "@posthog/ui/shell/analytics";
import { useHeaderStore } from "@posthog/ui/shell/headerStore";
Expand Down Expand Up @@ -299,8 +302,15 @@ export function WebsiteLayout() {

const channelId = params.channelId;
const dashboardId = params.dashboardId;
const taskId = params.taskId;
const base = channelId ? `/website/${channelId}` : "/website";

const { data: tasks } = useTasks();
const { tasks: filedTasks } = useChannelTasks(channelId);
const channelTask = filedTasks.some((record) => record.taskId === taskId)
? tasks?.find((task) => task.id === taskId)
: undefined;

const { channels } = useChannels();
const channelName = channelId
? (channels.find((c) => c.id === channelId)?.name ?? "Channel")
Expand Down Expand Up @@ -329,7 +339,14 @@ export function WebsiteLayout() {
gap="2"
className="h-10 shrink-0 border-gray-6 border-b px-3"
>
{headerContent}
<Flex
align="center"
justify="between"
className="h-full min-w-0 flex-1 overflow-hidden"
>
{headerContent}
</Flex>
{channelTask && <TaskHeaderActions task={channelTask} />}
</Flex>
)}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import type { Task } from "@posthog/shared/domain-types";
import { Theme } from "@radix-ui/themes";
import { render, screen } from "@testing-library/react";
import type { ReactNode } from "react";
import { describe, expect, it, vi } from "vitest";

const { useWorkspace, useWorkspaceLoaded } = vi.hoisted(() => ({
useWorkspace: vi.fn(),
useWorkspaceLoaded: vi.fn(),
}));

vi.mock("@posthog/ui/features/workspace/useWorkspace", () => ({
useWorkspace,
useWorkspaceLoaded,
}));
vi.mock("@posthog/ui/features/auth/store", () => ({
useAuthStateValue: (selector: (state: { status: string }) => unknown) =>
selector({ status: "authenticated" }),
}));
vi.mock("@posthog/ui/features/feature-flags/useFeatureFlag", () => ({
useFeatureFlag: () => true,
}));
vi.mock("@posthog/ui/features/sessions/useSession", () => ({
useSessionForTask: () => null,
}));
vi.mock("@posthog/ui/features/sessions/hooks/useSessionCallbacks", () => ({
useSessionCallbacks: () => ({ initiateHandoffToCloud: vi.fn() }),
}));
vi.mock("@posthog/ui/features/sessions/handoffDialogStore", () => ({
useHandoffDialogStore: (selector: (state: object) => unknown) =>
selector({
confirmOpen: false,
direction: null,
branchName: null,
openConfirm: vi.fn(),
closeConfirm: vi.fn(),
}),
}));
vi.mock("@posthog/ui/features/code-review/hooks/useDiffStatsToggle", () => ({
useDiffStatsToggle: () => ({
filesChanged: 0,
linesAdded: 0,
linesRemoved: 0,
isOpen: false,
toggle: vi.fn(),
}),
}));
vi.mock(
"@posthog/ui/features/skill-buttons/components/SkillButtonsMenu",
() => ({
SkillButtonsMenu: () => null,
}),
);
vi.mock("@posthog/ui/features/autoresearch/AutoresearchHeaderButton", () => ({
AutoresearchHeaderButton: () => null,
}));
vi.mock(
"@posthog/ui/features/git-interaction/components/BranchSelector",
() => ({
BranchSelector: () => null,
}),
);
vi.mock(
"@posthog/ui/features/git-interaction/components/CloudGitInteractionHeader",
() => ({ CloudGitInteractionHeader: () => <div>cloud actions</div> }),
);
vi.mock(
"@posthog/ui/features/git-interaction/components/TaskActionsMenu",
() => ({
TaskActionsMenu: () => <div>task menu</div>,
}),
);
vi.mock("@posthog/ui/features/sessions/components/StopCloudRunButton", () => ({
StopCloudRunButton: () => <div>stop cloud run</div>,
}));
vi.mock("@posthog/ui/features/diff-stats/DiffStatsBadge", () => ({
DiffStatsBadge: () => null,
}));
vi.mock("@posthog/ui/primitives/Tooltip", () => ({
Tooltip: ({ children }: { children: ReactNode }) => children,
}));

import { TaskHeaderActions } from "./TaskHeaderActions";

const task = { id: "task-1", title: "Fix the bug" } as Task;

function renderActions() {
render(
<Theme>
<TaskHeaderActions task={task} />
</Theme>,
);
}

describe("TaskHeaderActions", () => {
it("does not show workspace-dependent actions before workspaces load", () => {
useWorkspace.mockReturnValue(null);
useWorkspaceLoaded.mockReturnValue(false);

renderActions();

expect(screen.queryByText("Continue in cloud")).not.toBeInTheDocument();
expect(screen.queryByText("task menu")).not.toBeInTheDocument();
});

it("shows cloud controls for a loaded cloud workspace", () => {
useWorkspace.mockReturnValue({ mode: "cloud" });
useWorkspaceLoaded.mockReturnValue(true);

renderActions();

expect(screen.getByText("stop cloud run")).toBeInTheDocument();
expect(screen.getByText("cloud actions")).toBeInTheDocument();
expect(screen.getByText("task menu")).toBeInTheDocument();
expect(screen.queryByText("Continue in cloud")).not.toBeInTheDocument();
});
});
Loading
Loading