Skip to content

Commit ed6cdfb

Browse files
authored
Restore task action row (diff button) on channel task detail (#3381)
1 parent e01a82f commit ed6cdfb

5 files changed

Lines changed: 444 additions & 182 deletions

File tree

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import { Theme } from "@radix-ui/themes";
2+
import { render, screen } from "@testing-library/react";
3+
import { describe, expect, it, vi } from "vitest";
4+
5+
const { useChannelTasks, useParams, usePathname, useTasks } = vi.hoisted(
6+
() => ({
7+
useChannelTasks: vi.fn(),
8+
useParams: vi.fn(),
9+
usePathname: vi.fn(),
10+
useTasks: vi.fn(),
11+
}),
12+
);
13+
14+
vi.mock("@tanstack/react-router", () => ({
15+
Outlet: () => null,
16+
useNavigate: () => vi.fn(),
17+
useParams,
18+
useRouterState: ({
19+
select,
20+
}: {
21+
select: (s: { location: { pathname: string } }) => string;
22+
}) => select({ location: { pathname: usePathname() } }),
23+
}));
24+
25+
vi.mock(
26+
"@posthog/ui/features/task-detail/components/TaskHeaderActions",
27+
() => ({
28+
TaskHeaderActions: ({ task }: { task: { id: string } }) => (
29+
<div data-testid="task-header-actions">{task.id}</div>
30+
),
31+
}),
32+
);
33+
34+
vi.mock("@posthog/ui/features/tasks/useTasks", () => ({ useTasks }));
35+
vi.mock("@posthog/ui/features/canvas/hooks/useChannelTasks", () => ({
36+
useChannelTasks,
37+
}));
38+
vi.mock("@posthog/ui/features/canvas/hooks/useChannels", () => ({
39+
useChannels: () => ({
40+
channels: [{ id: "chan-1", name: "project-bluebird" }],
41+
}),
42+
}));
43+
vi.mock("@posthog/ui/features/canvas/hooks/useDashboards", () => ({
44+
useDashboard: () => ({ dashboard: undefined }),
45+
useDashboardMutations: () => ({}),
46+
}));
47+
vi.mock("@posthog/ui/features/canvas/stores/dashboardEditStore", () => ({
48+
useDashboardEditStore: (sel: (s: unknown) => unknown) =>
49+
sel({ setEditing: vi.fn() }),
50+
useIsDashboardEditing: () => false,
51+
}));
52+
vi.mock("@posthog/ui/features/canvas/stores/freeformChatStore", () => ({
53+
useFreeformChatStore: (sel: (s: unknown) => unknown) =>
54+
sel({ revert: vi.fn(), goToLatest: vi.fn() }),
55+
useFreeformThread: () => ({
56+
code: "",
57+
versions: [],
58+
currentVersionId: null,
59+
isSaving: false,
60+
}),
61+
}));
62+
vi.mock("@posthog/ui/features/canvas/components/NewCanvasMenu", () => ({
63+
NewCanvasMenu: () => null,
64+
}));
65+
vi.mock("@posthog/ui/features/canvas/freeform/CanvasFrameHost", () => ({
66+
CanvasFrameHost: () => null,
67+
}));
68+
69+
import { useHeaderStore } from "@posthog/ui/shell/headerStore";
70+
import { WebsiteLayout } from "./WebsiteLayout";
71+
72+
function renderLayout({
73+
pathname,
74+
params,
75+
tasks = [{ id: "task-1", title: "Fix the bug" }],
76+
channelTaskIds = tasks.map((task) => task.id),
77+
}: {
78+
pathname: string;
79+
params: Record<string, string>;
80+
tasks?: { id: string; title: string }[];
81+
channelTaskIds?: string[];
82+
}) {
83+
usePathname.mockReturnValue(pathname);
84+
useParams.mockReturnValue(params);
85+
useTasks.mockReturnValue({ data: tasks });
86+
useChannelTasks.mockReturnValue({
87+
tasks: channelTaskIds.map((taskId) => ({ taskId })),
88+
isLoading: false,
89+
});
90+
useHeaderStore.setState({ content: <span>crumb</span> });
91+
render(
92+
<Theme>
93+
<WebsiteLayout />
94+
</Theme>,
95+
);
96+
}
97+
98+
describe("WebsiteLayout task header actions", () => {
99+
it("renders the task action row on a channel task detail", () => {
100+
renderLayout({
101+
pathname: "/website/chan-1/tasks/task-1",
102+
params: { channelId: "chan-1", taskId: "task-1" },
103+
});
104+
expect(screen.getByTestId("task-header-actions")).toHaveTextContent(
105+
"task-1",
106+
);
107+
});
108+
109+
it("does not render actions for a task filed to another channel", () => {
110+
renderLayout({
111+
pathname: "/website/chan-1/tasks/task-1",
112+
params: { channelId: "chan-1", taskId: "task-1" },
113+
channelTaskIds: ["other-task"],
114+
});
115+
expect(screen.queryByTestId("task-header-actions")).not.toBeInTheDocument();
116+
});
117+
118+
it.each([
119+
["channel home", "/website/chan-1", { channelId: "chan-1" }],
120+
["new task", "/website/chan-1/new", { channelId: "chan-1" }],
121+
])("does not render the action row on %s", (_label, pathname, params) => {
122+
renderLayout({ pathname, params });
123+
expect(screen.queryByTestId("task-header-actions")).not.toBeInTheDocument();
124+
});
125+
});

packages/ui/src/features/canvas/components/WebsiteLayout.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { iconForTemplate } from "@posthog/ui/features/canvas/components/canvasTe
1919
import { NewCanvasMenu } from "@posthog/ui/features/canvas/components/NewCanvasMenu";
2020
import { CanvasFrameHost } from "@posthog/ui/features/canvas/freeform/CanvasFrameHost";
2121
import { useChannels } from "@posthog/ui/features/canvas/hooks/useChannels";
22+
import { useChannelTasks } from "@posthog/ui/features/canvas/hooks/useChannelTasks";
2223
import {
2324
useDashboard,
2425
useDashboardMutations,
@@ -32,6 +33,8 @@ import {
3233
useFreeformThread,
3334
} from "@posthog/ui/features/canvas/stores/freeformChatStore";
3435
import { copyCanvasLink } from "@posthog/ui/features/canvas/utils/copyCanvasLink";
36+
import { TaskHeaderActions } from "@posthog/ui/features/task-detail/components/TaskHeaderActions";
37+
import { useTasks } from "@posthog/ui/features/tasks/useTasks";
3538
import { toast } from "@posthog/ui/primitives/toast";
3639
import { track } from "@posthog/ui/shell/analytics";
3740
import { useHeaderStore } from "@posthog/ui/shell/headerStore";
@@ -299,8 +302,15 @@ export function WebsiteLayout() {
299302

300303
const channelId = params.channelId;
301304
const dashboardId = params.dashboardId;
305+
const taskId = params.taskId;
302306
const base = channelId ? `/website/${channelId}` : "/website";
303307

308+
const { data: tasks } = useTasks();
309+
const { tasks: filedTasks } = useChannelTasks(channelId);
310+
const channelTask = filedTasks.some((record) => record.taskId === taskId)
311+
? tasks?.find((task) => task.id === taskId)
312+
: undefined;
313+
304314
const { channels } = useChannels();
305315
const channelName = channelId
306316
? (channels.find((c) => c.id === channelId)?.name ?? "Channel")
@@ -329,7 +339,14 @@ export function WebsiteLayout() {
329339
gap="2"
330340
className="h-10 shrink-0 border-gray-6 border-b px-3"
331341
>
332-
{headerContent}
342+
<Flex
343+
align="center"
344+
justify="between"
345+
className="h-full min-w-0 flex-1 overflow-hidden"
346+
>
347+
{headerContent}
348+
</Flex>
349+
{channelTask && <TaskHeaderActions task={channelTask} />}
333350
</Flex>
334351
)}
335352

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import type { Task } from "@posthog/shared/domain-types";
2+
import { Theme } from "@radix-ui/themes";
3+
import { render, screen } from "@testing-library/react";
4+
import type { ReactNode } from "react";
5+
import { describe, expect, it, vi } from "vitest";
6+
7+
const { useWorkspace, useWorkspaceLoaded } = vi.hoisted(() => ({
8+
useWorkspace: vi.fn(),
9+
useWorkspaceLoaded: vi.fn(),
10+
}));
11+
12+
vi.mock("@posthog/ui/features/workspace/useWorkspace", () => ({
13+
useWorkspace,
14+
useWorkspaceLoaded,
15+
}));
16+
vi.mock("@posthog/ui/features/auth/store", () => ({
17+
useAuthStateValue: (selector: (state: { status: string }) => unknown) =>
18+
selector({ status: "authenticated" }),
19+
}));
20+
vi.mock("@posthog/ui/features/feature-flags/useFeatureFlag", () => ({
21+
useFeatureFlag: () => true,
22+
}));
23+
vi.mock("@posthog/ui/features/sessions/useSession", () => ({
24+
useSessionForTask: () => null,
25+
}));
26+
vi.mock("@posthog/ui/features/sessions/hooks/useSessionCallbacks", () => ({
27+
useSessionCallbacks: () => ({ initiateHandoffToCloud: vi.fn() }),
28+
}));
29+
vi.mock("@posthog/ui/features/sessions/handoffDialogStore", () => ({
30+
useHandoffDialogStore: (selector: (state: object) => unknown) =>
31+
selector({
32+
confirmOpen: false,
33+
direction: null,
34+
branchName: null,
35+
openConfirm: vi.fn(),
36+
closeConfirm: vi.fn(),
37+
}),
38+
}));
39+
vi.mock("@posthog/ui/features/code-review/hooks/useDiffStatsToggle", () => ({
40+
useDiffStatsToggle: () => ({
41+
filesChanged: 0,
42+
linesAdded: 0,
43+
linesRemoved: 0,
44+
isOpen: false,
45+
toggle: vi.fn(),
46+
}),
47+
}));
48+
vi.mock(
49+
"@posthog/ui/features/skill-buttons/components/SkillButtonsMenu",
50+
() => ({
51+
SkillButtonsMenu: () => null,
52+
}),
53+
);
54+
vi.mock("@posthog/ui/features/autoresearch/AutoresearchHeaderButton", () => ({
55+
AutoresearchHeaderButton: () => null,
56+
}));
57+
vi.mock(
58+
"@posthog/ui/features/git-interaction/components/BranchSelector",
59+
() => ({
60+
BranchSelector: () => null,
61+
}),
62+
);
63+
vi.mock(
64+
"@posthog/ui/features/git-interaction/components/CloudGitInteractionHeader",
65+
() => ({ CloudGitInteractionHeader: () => <div>cloud actions</div> }),
66+
);
67+
vi.mock(
68+
"@posthog/ui/features/git-interaction/components/TaskActionsMenu",
69+
() => ({
70+
TaskActionsMenu: () => <div>task menu</div>,
71+
}),
72+
);
73+
vi.mock("@posthog/ui/features/sessions/components/StopCloudRunButton", () => ({
74+
StopCloudRunButton: () => <div>stop cloud run</div>,
75+
}));
76+
vi.mock("@posthog/ui/features/diff-stats/DiffStatsBadge", () => ({
77+
DiffStatsBadge: () => null,
78+
}));
79+
vi.mock("@posthog/ui/primitives/Tooltip", () => ({
80+
Tooltip: ({ children }: { children: ReactNode }) => children,
81+
}));
82+
83+
import { TaskHeaderActions } from "./TaskHeaderActions";
84+
85+
const task = { id: "task-1", title: "Fix the bug" } as Task;
86+
87+
function renderActions() {
88+
render(
89+
<Theme>
90+
<TaskHeaderActions task={task} />
91+
</Theme>,
92+
);
93+
}
94+
95+
describe("TaskHeaderActions", () => {
96+
it("does not show workspace-dependent actions before workspaces load", () => {
97+
useWorkspace.mockReturnValue(null);
98+
useWorkspaceLoaded.mockReturnValue(false);
99+
100+
renderActions();
101+
102+
expect(screen.queryByText("Continue in cloud")).not.toBeInTheDocument();
103+
expect(screen.queryByText("task menu")).not.toBeInTheDocument();
104+
});
105+
106+
it("shows cloud controls for a loaded cloud workspace", () => {
107+
useWorkspace.mockReturnValue({ mode: "cloud" });
108+
useWorkspaceLoaded.mockReturnValue(true);
109+
110+
renderActions();
111+
112+
expect(screen.getByText("stop cloud run")).toBeInTheDocument();
113+
expect(screen.getByText("cloud actions")).toBeInTheDocument();
114+
expect(screen.getByText("task menu")).toBeInTheDocument();
115+
expect(screen.queryByText("Continue in cloud")).not.toBeInTheDocument();
116+
});
117+
});

0 commit comments

Comments
 (0)