diff --git a/packages/core/src/sidebar/groupTasks.ts b/packages/core/src/sidebar/groupTasks.ts index 3c315d8d54..e7e8be8ffb 100644 --- a/packages/core/src/sidebar/groupTasks.ts +++ b/packages/core/src/sidebar/groupTasks.ts @@ -50,9 +50,20 @@ export function getRepositoryInfo( return null; } +export function folderGroupId(folder: { + path: string; + remoteUrl: string | null; +}): string { + if (folder.remoteUrl) { + return normalizeRepoKey(folder.remoteUrl).toLowerCase(); + } + return folder.path; +} + export function groupByRepository( tasks: T[], folderOrder: string[], + allFolders: { path: string; remoteUrl: string | null; name: string }[] = [], ): TaskGroup[] { const groupMap = new Map>(); @@ -70,6 +81,13 @@ export function groupByRepository( group.tasks.push(task); } + for (const folder of allFolders) { + const groupId = folderGroupId(folder); + if (!groupMap.has(groupId)) { + groupMap.set(groupId, { id: groupId, name: folder.name, tasks: [] }); + } + } + const groups = Array.from(groupMap.values()); // Disambiguate groups that share a display name (e.g. `posthog/posthog` diff --git a/packages/ui/src/features/sidebar/components/SidebarMenu.tsx b/packages/ui/src/features/sidebar/components/SidebarMenu.tsx index 9edb00f7f7..8e6ad81dc7 100644 --- a/packages/ui/src/features/sidebar/components/SidebarMenu.tsx +++ b/packages/ui/src/features/sidebar/components/SidebarMenu.tsx @@ -1,3 +1,4 @@ +import { folderGroupId } from "@posthog/core/sidebar/groupTasks"; import { isTaskActivelyRunning } from "@posthog/core/sidebar/taskRunning"; import { useHostTRPCClient } from "@posthog/host-router/react"; import { Separator } from "@posthog/quill"; @@ -8,6 +9,8 @@ import { useArchiveTask, } from "@posthog/ui/features/archive/useArchiveTask"; import { useCommandCenterStore } from "@posthog/ui/features/command-center/commandCenterStore"; +import { useExternalAppAction } from "@posthog/ui/features/external-apps/useExternalAppAction"; +import { useFolders } from "@posthog/ui/features/folders/useFolders"; import { useArchivingTasksStore } from "@posthog/ui/features/sidebar/archivingTasksStore"; import { useSidebarStore } from "@posthog/ui/features/sidebar/sidebarStore"; import { useTaskSelectionStore } from "@posthog/ui/features/sidebar/taskSelectionStore"; @@ -52,6 +55,10 @@ function SidebarMenuComponent() { const { data: workspaces = {} } = useWorkspaces(); const { markAsViewed } = useTaskViewed(); + const { folders, removeFolder } = useFolders(); + + const openExternalApp = useExternalAppAction(); + const { showContextMenu, editingTaskId, setEditingTaskId } = useTaskContextMenu(); const { archiveTask } = useArchiveTask(); @@ -216,6 +223,35 @@ function SidebarMenuComponent() { [hostClient, queryClient, clearSelection, archiveCacheKeys], ); + const handleGroupContextMenu = useCallback( + async (groupId: string, e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + const folder = folders.find((f) => folderGroupId(f) === groupId); + if (!folder) return; + try { + const result = + await hostClient.contextMenu.showFolderContextMenu.mutate({ + folderName: folder.name, + folderPath: folder.path, + }); + if (result.action?.type === "remove") { + await removeFolder(folder.id); + } else if (result.action?.type === "external-app") { + await openExternalApp( + result.action.action, + folder.path, + folder.name, + { workspace: null }, + ); + } + } catch (error) { + log.error("Failed to show folder context menu", error); + } + }, + [folders, removeFolder, hostClient, openExternalApp], + ); + const handleTaskContextMenu = ( taskId: string, e: React.MouseEvent, @@ -431,6 +467,7 @@ function SidebarMenuComponent() { onTaskTogglePin={handleTaskTogglePin} onTaskEditSubmit={handleTaskEditSubmit} onTaskEditCancel={handleTaskEditCancel} + onGroupContextMenu={handleGroupContextMenu} hasMore={sidebarData.hasMore} /> )} diff --git a/packages/ui/src/features/sidebar/components/TaskListView.tsx b/packages/ui/src/features/sidebar/components/TaskListView.tsx index df15ed2db6..7169703845 100644 --- a/packages/ui/src/features/sidebar/components/TaskListView.tsx +++ b/packages/ui/src/features/sidebar/components/TaskListView.tsx @@ -2,14 +2,16 @@ import { PointerSensor } from "@dnd-kit/dom"; import type { DragDropEvents } from "@dnd-kit/react"; import { DragDropProvider } from "@dnd-kit/react"; import { GitBranch } from "@phosphor-icons/react"; -import { groupTasksByRelativeDate } from "@posthog/core/sidebar/groupTasks"; +import { + folderGroupId, + groupTasksByRelativeDate, +} from "@posthog/core/sidebar/groupTasks"; import { mostRecentRunEnvironment } from "@posthog/core/sidebar/runEnvironment"; import type { TaskData, TaskGroup, } from "@posthog/core/sidebar/sidebarData.types"; import { MenuLabel } from "@posthog/quill"; -import { normalizeRepoKey } from "@posthog/shared"; import { builderHog } from "@posthog/ui/assets/hedgehogs"; import { useFolders } from "@posthog/ui/features/folders/useFolders"; import { useArchivingTasksStore } from "@posthog/ui/features/sidebar/archivingTasksStore"; @@ -47,6 +49,7 @@ interface TaskListViewProps { newTitle: string, ) => void; onTaskEditCancel: () => void; + onGroupContextMenu?: (groupId: string, e: React.MouseEvent) => void; hasMore: boolean; } @@ -143,6 +146,7 @@ export function TaskListView({ onTaskTogglePin, onTaskEditSubmit, onTaskEditCancel, + onGroupContextMenu, hasMore, }: TaskListViewProps) { const selectedIdSet = useMemo( @@ -273,12 +277,7 @@ export function TaskListView({ {groupedTasks.map((group, index) => { const isExpanded = !collapsedSections.has(group.id); - const folder = folders.find( - (f) => - (f.remoteUrl && - normalizeRepoKey(f.remoteUrl).toLowerCase() === group.id) || - f.path === group.id, - ); + const folder = folders.find((f) => folderGroupId(f) === group.id); const groupFolderId = folder?.id ?? group.tasks.find((t) => t.folderId)?.folderId; return ( @@ -304,30 +303,41 @@ export function TaskListView({ } }} newTaskTooltip={`Start new task in ${folder?.name ?? group.name}`} + onContextMenu={ + onGroupContextMenu + ? (e) => onGroupContextMenu(group.id, e) + : undefined + } > - {group.tasks.map((task) => ( - onTaskClick(task.id, e)} - onDoubleClick={() => onTaskDoubleClick(task.id)} - onContextMenu={(e, isPinned) => - onTaskContextMenu(task.id, e, isPinned) - } - onArchive={() => onTaskArchive(task.id)} - onTogglePin={() => onTaskTogglePin(task.id)} - onEditSubmit={(newTitle) => - onTaskEditSubmit(task.id, task.title, newTitle) - } - onEditCancel={onTaskEditCancel} - timestamp={task[timestampKey]} - depth={1} - /> - ))} + {group.tasks.length === 0 ? ( +

+ No agents yet +

+ ) : ( + group.tasks.map((task) => ( + onTaskClick(task.id, e)} + onDoubleClick={() => onTaskDoubleClick(task.id)} + onContextMenu={(e, isPinned) => + onTaskContextMenu(task.id, e, isPinned) + } + onArchive={() => onTaskArchive(task.id)} + onTogglePin={() => onTaskTogglePin(task.id)} + onEditSubmit={(newTitle) => + onTaskEditSubmit(task.id, task.title, newTitle) + } + onEditCancel={onTaskEditCancel} + timestamp={task[timestampKey]} + depth={1} + /> + )) + )} ); diff --git a/packages/ui/src/features/sidebar/components/TasksHeader.tsx b/packages/ui/src/features/sidebar/components/TasksHeader.tsx index c3400b6792..27c99997da 100644 --- a/packages/ui/src/features/sidebar/components/TasksHeader.tsx +++ b/packages/ui/src/features/sidebar/components/TasksHeader.tsx @@ -1,7 +1,9 @@ import { + FolderPlus, FunnelSimple as FunnelSimpleIcon, MagnifyingGlass, } from "@phosphor-icons/react"; +import { useHostTRPCClient } from "@posthog/host-router/react"; import { Button, DropdownMenu, @@ -13,8 +15,50 @@ import { MenuLabel, } from "@posthog/quill"; import { useMeQuery } from "@posthog/ui/features/auth/useMeQuery"; +import { useFolders } from "@posthog/ui/features/folders/useFolders"; import { useSidebarStore } from "@posthog/ui/features/sidebar/sidebarStore"; +import { Tooltip } from "@posthog/ui/primitives/Tooltip"; +import { toast } from "@posthog/ui/primitives/toast"; import { useCommandMenuStore } from "@posthog/ui/shell/commandMenuStore"; +import { logger } from "@posthog/ui/shell/logger"; +import { useState } from "react"; + +const log = logger.scope("tasks-header"); + +function AddFolderButton() { + const trpcClient = useHostTRPCClient(); + const { addFolder } = useFolders(); + const [isOpening, setIsOpening] = useState(false); + + const handleClick = async () => { + if (isOpening) return; + setIsOpening(true); + try { + const selectedPath = await trpcClient.os.selectDirectory.query(); + if (selectedPath) await addFolder(selectedPath); + } catch (error) { + log.error("Failed to add folder", error); + toast.error("Couldn't add folder"); + } finally { + setIsOpening(false); + } + }; + + return ( + + + + ); +} function TaskSearchButton() { const openCommandMenu = useCommandMenuStore((state) => state.open); @@ -131,6 +175,7 @@ export function TasksHeader() { Tasks + diff --git a/packages/ui/src/features/sidebar/useSidebarData.ts b/packages/ui/src/features/sidebar/useSidebarData.ts index 8aac61fb8b..c05be96909 100644 --- a/packages/ui/src/features/sidebar/useSidebarData.ts +++ b/packages/ui/src/features/sidebar/useSidebarData.ts @@ -17,6 +17,7 @@ import { computeSummaryIds } from "@posthog/core/sidebar/summaryIds"; import type { AppView } from "@posthog/ui/router/useAppView"; import { useEffect, useMemo, useRef } from "react"; import { useArchivedTaskIds } from "../archive/useArchivedTaskIds"; +import { useFolders } from "../folders/useFolders"; import { useProvisioningStore } from "../provisioning/store"; import { useSuspendedTaskIds } from "../suspension/useSuspendedTaskIds"; import { useSlackTasks, useTaskSummaries, useTasks } from "../tasks/useTasks"; @@ -180,9 +181,16 @@ export function useSidebarData({ [sortedUnpinnedTasks, organizeMode, historyVisibleCount], ); + const { folders } = useFolders(); + const groupedTasks = useMemo( - () => groupByRepository(sortedUnpinnedTasks, folderOrder), - [sortedUnpinnedTasks, folderOrder], + () => + groupByRepository( + sortedUnpinnedTasks, + folderOrder, + organizeMode === "by-project" ? folders : [], + ), + [sortedUnpinnedTasks, folderOrder, folders, organizeMode], ); const groupIdsRef = useRef([]);