Skip to content

Commit b9339e9

Browse files
authored
feat(sidebar): filter tasks by workspace type (#3248)
1 parent 63cf963 commit b9339e9

6 files changed

Lines changed: 163 additions & 3 deletions

File tree

packages/core/src/sidebar/buildSidebarData.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { readPrUrls } from "@posthog/shared";
1+
import { readPrUrls, type WorkspaceMode } from "@posthog/shared";
22
import type { Task, TaskRunStatus } from "@posthog/shared/domain-types";
33
import { getRepositoryInfo } from "./groupTasks";
44
import type { TaskData } from "./sidebarData.types";
@@ -117,6 +117,7 @@ export interface TaskWorkspace {
117117
folderPath?: string | null;
118118
branchName?: string | null;
119119
linkedBranch?: string | null;
120+
mode?: WorkspaceMode;
120121
}
121122

122123
export interface TaskTimestamp {
@@ -175,6 +176,12 @@ export function deriveTaskData(
175176
folderId: workspace?.folderId || undefined,
176177
taskRunStatus: session?.cloudStatus ?? task.latest_run?.status ?? undefined,
177178
taskRunEnvironment: task.latest_run?.environment ?? undefined,
179+
// The `latest_run` fallback only matters in the `showAllUsers` view: the
180+
// default view's `filterVisibleTasks` already restricts to tasks with a
181+
// local `workspace`, so a pure-cloud task without one only shows up there.
182+
workspaceMode:
183+
workspace?.mode ??
184+
(task.latest_run?.environment === "cloud" ? "cloud" : undefined),
178185
originProduct,
179186
slackThreadUrl,
180187
folderPath: workspace?.folderPath ?? null,
@@ -184,6 +191,34 @@ export function deriveTaskData(
184191
};
185192
}
186193

194+
// A Record keyed by the full `WorkspaceMode` union, so adding a mode to the
195+
// schema forces a compile error here instead of silently falling out of sync
196+
// with `ALL_WORKSPACE_MODES` (and the filter's "all enabled" short-circuit).
197+
const WORKSPACE_MODE_MEMBERSHIP: Record<WorkspaceMode, true> = {
198+
worktree: true,
199+
local: true,
200+
cloud: true,
201+
};
202+
203+
export const ALL_WORKSPACE_MODES: readonly WorkspaceMode[] = Object.keys(
204+
WORKSPACE_MODE_MEMBERSHIP,
205+
) as WorkspaceMode[];
206+
207+
/**
208+
* Keeps tasks whose workspace mode is in `enabledModes`. Tasks without a known
209+
* mode always pass so an unclassified task never silently disappears.
210+
*/
211+
export function filterByWorkspaceMode(
212+
tasks: TaskData[],
213+
enabledModes: readonly WorkspaceMode[],
214+
): TaskData[] {
215+
if (enabledModes.length >= ALL_WORKSPACE_MODES.length) return tasks;
216+
return tasks.filter(
217+
(task) =>
218+
task.workspaceMode == null || enabledModes.includes(task.workspaceMode),
219+
);
220+
}
221+
187222
function getSortValue(task: TaskData, sortMode: SortMode): number {
188223
return sortMode === "updated" ? task.lastActivityAt : task.createdAt;
189224
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import type { WorkspaceMode } from "@posthog/shared";
2+
import { describe, expect, it } from "vitest";
3+
import { ALL_WORKSPACE_MODES, filterByWorkspaceMode } from "./buildSidebarData";
4+
import type { TaskData } from "./sidebarData.types";
5+
6+
const task = (overrides: Partial<TaskData>): TaskData => ({
7+
id: "t",
8+
title: "t",
9+
createdAt: 0,
10+
lastActivityAt: 0,
11+
isGenerating: false,
12+
isUnread: false,
13+
isPinned: false,
14+
needsPermission: false,
15+
repository: null,
16+
isSuspended: false,
17+
folderPath: null,
18+
cloudPrUrl: null,
19+
branchName: null,
20+
linkedBranch: null,
21+
...overrides,
22+
});
23+
24+
describe("filterByWorkspaceMode", () => {
25+
const worktree = task({ id: "w", workspaceMode: "worktree" });
26+
const local = task({ id: "l", workspaceMode: "local" });
27+
const cloud = task({ id: "c", workspaceMode: "cloud" });
28+
const unknown = task({ id: "u", workspaceMode: undefined });
29+
const tasks = [worktree, local, cloud, unknown];
30+
31+
it.each<{
32+
name: string;
33+
enabledModes: readonly WorkspaceMode[];
34+
expected: TaskData[];
35+
}>([
36+
{
37+
name: "returns all tasks when every mode is enabled",
38+
enabledModes: ALL_WORKSPACE_MODES,
39+
expected: tasks,
40+
},
41+
{
42+
name: "keeps only tasks whose mode is enabled, plus unknown-mode tasks",
43+
enabledModes: ["local"],
44+
expected: [local, unknown],
45+
},
46+
{
47+
name: "keeps multiple enabled modes",
48+
enabledModes: ["worktree", "cloud"],
49+
expected: [worktree, cloud, unknown],
50+
},
51+
{
52+
name: "keeps only unknown-mode tasks when nothing is enabled",
53+
enabledModes: [],
54+
expected: [unknown],
55+
},
56+
])("$name", ({ enabledModes, expected }) => {
57+
expect(filterByWorkspaceMode(tasks, enabledModes)).toEqual(expected);
58+
});
59+
});

packages/core/src/sidebar/sidebarData.types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { WorkspaceMode } from "@posthog/shared";
12
import type { TaskRunStatus } from "@posthog/shared/domain-types";
23
import type {
34
TaskGroup as GenericTaskGroup,
@@ -18,6 +19,7 @@ export interface TaskData {
1819
folderId?: string;
1920
taskRunStatus?: TaskRunStatus;
2021
taskRunEnvironment?: "local" | "cloud";
22+
workspaceMode?: WorkspaceMode;
2123
originProduct?: string;
2224
slackThreadUrl?: string;
2325
folderPath: string | null;

packages/ui/src/features/sidebar/components/TasksHeader.tsx

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,29 @@
11
import {
2+
Cloud,
3+
Desktop,
24
FolderPlus,
35
FunnelSimple as FunnelSimpleIcon,
6+
GitBranch,
7+
type Icon,
48
MagnifyingGlass,
59
} from "@phosphor-icons/react";
10+
import { ALL_WORKSPACE_MODES } from "@posthog/core/sidebar/buildSidebarData";
611
import { useHostTRPCClient } from "@posthog/host-router/react";
712
import {
813
Button,
914
DropdownMenu,
15+
DropdownMenuCheckboxItem,
1016
DropdownMenuContent,
1117
DropdownMenuRadioGroup,
1218
DropdownMenuRadioItem,
1319
DropdownMenuSeparator,
20+
DropdownMenuSub,
21+
DropdownMenuSubContent,
22+
DropdownMenuSubTrigger,
1423
DropdownMenuTrigger,
1524
MenuLabel,
1625
} from "@posthog/quill";
26+
import type { WorkspaceMode } from "@posthog/shared";
1727
import { useMeQuery } from "@posthog/ui/features/auth/useMeQuery";
1828
import { useFolders } from "@posthog/ui/features/folders/useFolders";
1929
import { useSidebarStore } from "@posthog/ui/features/sidebar/sidebarStore";
@@ -25,6 +35,14 @@ import { useState } from "react";
2535

2636
const log = logger.scope("tasks-header");
2737

38+
// Record (not a hand-maintained array) so adding a WorkspaceMode forces a
39+
// compile error here instead of silently missing a checkbox.
40+
const ENVIRONMENT_META: Record<WorkspaceMode, { label: string; icon: Icon }> = {
41+
worktree: { label: "Worktree", icon: GitBranch },
42+
local: { label: "Local", icon: Desktop },
43+
cloud: { label: "Cloud", icon: Cloud },
44+
};
45+
2846
function AddFolderButton() {
2947
const trpcClient = useHostTRPCClient();
3048
const { addFolder } = useFolders();
@@ -82,6 +100,8 @@ function TaskFilterMenu() {
82100
const setSortMode = useSidebarStore((state) => state.setSortMode);
83101
const setShowAllUsers = useSidebarStore((state) => state.setShowAllUsers);
84102
const setShowInternal = useSidebarStore((state) => state.setShowInternal);
103+
const taskTypeFilter = useSidebarStore((state) => state.taskTypeFilter);
104+
const toggleTaskType = useSidebarStore((state) => state.toggleTaskType);
85105
const { data: currentUser } = useMeQuery();
86106
const isStaff = currentUser?.is_staff === true;
87107

@@ -163,6 +183,28 @@ function TaskFilterMenu() {
163183
</DropdownMenuRadioGroup>
164184
</>
165185
)}
186+
187+
<DropdownMenuSeparator />
188+
189+
<DropdownMenuSub>
190+
<DropdownMenuSubTrigger>Environment</DropdownMenuSubTrigger>
191+
<DropdownMenuSubContent side="right" sideOffset={4}>
192+
{ALL_WORKSPACE_MODES.map((mode) => {
193+
const { label, icon: Icon } = ENVIRONMENT_META[mode];
194+
return (
195+
<DropdownMenuCheckboxItem
196+
key={mode}
197+
checked={taskTypeFilter.includes(mode)}
198+
closeOnClick={false}
199+
onCheckedChange={() => toggleTaskType(mode)}
200+
>
201+
<Icon size={14} />
202+
{label}
203+
</DropdownMenuCheckboxItem>
204+
);
205+
})}
206+
</DropdownMenuSubContent>
207+
</DropdownMenuSub>
166208
</DropdownMenuContent>
167209
</DropdownMenu>
168210
);

packages/ui/src/features/sidebar/sidebarStore.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { ALL_WORKSPACE_MODES } from "@posthog/core/sidebar/buildSidebarData";
2+
import type { WorkspaceMode } from "@posthog/shared";
13
import { create } from "zustand";
24
import { persist } from "zustand/middleware";
35
import { SIDEBAR_MIN_WIDTH } from "./constants";
@@ -14,6 +16,7 @@ interface SidebarStoreState {
1416
sortMode: "updated" | "created";
1517
showAllUsers: boolean;
1618
showInternal: boolean;
19+
taskTypeFilter: WorkspaceMode[];
1720
}
1821

1922
interface SidebarStoreActions {
@@ -32,6 +35,7 @@ interface SidebarStoreActions {
3235
setSortMode: (mode: SidebarStoreState["sortMode"]) => void;
3336
setShowAllUsers: (showAllUsers: boolean) => void;
3437
setShowInternal: (showInternal: boolean) => void;
38+
toggleTaskType: (mode: WorkspaceMode) => void;
3539
}
3640

3741
type SidebarStore = SidebarStoreState & SidebarStoreActions;
@@ -50,6 +54,7 @@ export const useSidebarStore = create<SidebarStore>()(
5054
sortMode: "updated",
5155
showAllUsers: false,
5256
showInternal: false,
57+
taskTypeFilter: [...ALL_WORKSPACE_MODES],
5358
setOpen: (open) => set({ open, hasUserSetOpen: true }),
5459
setOpenAuto: (open) =>
5560
set((state) => (state.hasUserSetOpen ? state : { open })),
@@ -100,6 +105,12 @@ export const useSidebarStore = create<SidebarStore>()(
100105
setSortMode: (sortMode) => set({ sortMode }),
101106
setShowAllUsers: (showAllUsers) => set({ showAllUsers }),
102107
setShowInternal: (showInternal) => set({ showInternal }),
108+
toggleTaskType: (mode) =>
109+
set((state) => ({
110+
taskTypeFilter: state.taskTypeFilter.includes(mode)
111+
? state.taskTypeFilter.filter((m) => m !== mode)
112+
: [...state.taskTypeFilter, mode],
113+
})),
103114
}),
104115
{
105116
name: "sidebar-storage",
@@ -114,6 +125,7 @@ export const useSidebarStore = create<SidebarStore>()(
114125
sortMode: state.sortMode,
115126
showAllUsers: state.showAllUsers,
116127
showInternal: state.showInternal,
128+
taskTypeFilter: state.taskTypeFilter,
117129
}),
118130
merge: (persisted, current) => {
119131
const persistedState = persisted as {
@@ -127,6 +139,7 @@ export const useSidebarStore = create<SidebarStore>()(
127139
sortMode?: SidebarStoreState["sortMode"];
128140
showAllUsers?: boolean;
129141
showInternal?: boolean;
142+
taskTypeFilter?: WorkspaceMode[];
130143
};
131144
return {
132145
...current,
@@ -145,6 +158,8 @@ export const useSidebarStore = create<SidebarStore>()(
145158
sortMode: persistedState.sortMode ?? current.sortMode,
146159
showAllUsers: persistedState.showAllUsers ?? current.showAllUsers,
147160
showInternal: persistedState.showInternal ?? current.showInternal,
161+
taskTypeFilter:
162+
persistedState.taskTypeFilter ?? current.taskTypeFilter,
148163
};
149164
},
150165
},

packages/ui/src/features/sidebar/useSidebarData.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
deriveTaskData,
33
type FullTask,
4+
filterByWorkspaceMode,
45
filterVisibleTasks,
56
narrowFullTask,
67
partitionAndSortTasks,
@@ -51,6 +52,7 @@ export function useSidebarData({
5152
const organizeMode = useSidebarStore((state) => state.organizeMode);
5253
const sortMode = useSidebarStore((state) => state.sortMode);
5354
const folderOrder = useSidebarStore((state) => state.folderOrder);
55+
const taskTypeFilter = useSidebarStore((state) => state.taskTypeFilter);
5456

5557
const summaryIds = useMemo(
5658
() =>
@@ -166,9 +168,14 @@ export function useSidebarData({
166168
],
167169
);
168170

171+
const filteredTaskData = useMemo(
172+
() => filterByWorkspaceMode(taskData, taskTypeFilter),
173+
[taskData, taskTypeFilter],
174+
);
175+
169176
const { pinnedTasks, sortedUnpinnedTasks, totalCount } = useMemo(
170-
() => partitionAndSortTasks(taskData, sortMode),
171-
[taskData, sortMode],
177+
() => partitionAndSortTasks(filteredTaskData, sortMode),
178+
[filteredTaskData, sortMode],
172179
);
173180

174181
const { flatTasks, hasMore } = useMemo(

0 commit comments

Comments
 (0)