From 61ddbe9f010e018fbd947f8254c27b89c5301fb5 Mon Sep 17 00:00:00 2001 From: Adam Bowker Date: Mon, 6 Apr 2026 14:11:42 -0700 Subject: [PATCH] feat(code): add sort and filter to archived tasks view --- .../components/ArchivedTasksView.stories.tsx | 21 +- .../archive/components/ArchivedTasksView.tsx | 329 ++++++++++++++++-- apps/code/src/renderer/styles/globals.css | 8 + 3 files changed, 321 insertions(+), 37 deletions(-) diff --git a/apps/code/src/renderer/features/archive/components/ArchivedTasksView.stories.tsx b/apps/code/src/renderer/features/archive/components/ArchivedTasksView.stories.tsx index ce1638638..bf8e72277 100644 --- a/apps/code/src/renderer/features/archive/components/ArchivedTasksView.stories.tsx +++ b/apps/code/src/renderer/features/archive/components/ArchivedTasksView.stories.tsx @@ -74,7 +74,7 @@ const meta: Meta = { isLoading: false, branchNotFound: null, onUnarchive: () => {}, - onDelete: (_taskId: string, _taskTitle: string) => {}, + onDelete: (_taskId: string) => {}, onContextMenu: () => {}, onBranchNotFoundClose: () => {}, onRecreateBranch: () => {}, @@ -147,3 +147,22 @@ export const LongLabels: Story = { ], }, }; + +export const MixedModes: Story = { + args: { + items: [ + { + archived: { ...createArchivedTask("t1", 1), mode: "cloud" }, + task: createTask("t1", "Cloud deploy pipeline", 10, "infra"), + }, + { + archived: { ...createArchivedTask("t2", 5), mode: "local" }, + task: createTask("t2", "Local debugging session", 3, "frontend"), + }, + { + archived: { ...createArchivedTask("t3", 0), mode: "worktree" }, + task: createTask("t3", "Worktree refactor", 20, "backend"), + }, + ], + }, +}; diff --git a/apps/code/src/renderer/features/archive/components/ArchivedTasksView.tsx b/apps/code/src/renderer/features/archive/components/ArchivedTasksView.tsx index eca4a8f01..39c1e202d 100644 --- a/apps/code/src/renderer/features/archive/components/ArchivedTasksView.tsx +++ b/apps/code/src/renderer/features/archive/components/ArchivedTasksView.tsx @@ -4,11 +4,25 @@ import { useTasks } from "@features/tasks/hooks/useTasks"; import { useSetHeaderContent } from "@hooks/useSetHeaderContent"; import type { WorkspaceMode } from "@main/services/workspace/schemas"; import { + CaretDown, + CaretUp, + Check, Cloud as CloudIcon, GitBranch as GitBranchIcon, Laptop as LaptopIcon, + MagnifyingGlass, } from "@phosphor-icons/react"; -import { Box, Button, Dialog, Flex, Table, Text } from "@radix-ui/themes"; +import { + AlertDialog, + Box, + Button, + Dialog, + Flex, + Popover, + Table, + Text, + TextField, +} from "@radix-ui/themes"; import { trpcClient, useTRPC } from "@renderer/trpc"; import type { Task } from "@shared/types"; import type { ArchivedTask } from "@shared/types/archive"; @@ -83,6 +97,117 @@ function ModeIcon({ mode }: { mode: WorkspaceMode }) { ); } +type SortColumn = "created" | "archived"; +type SortDirection = "asc" | "desc"; + +interface SortState { + column: SortColumn; + direction: SortDirection; +} + +function SortableColumnHeader({ + column, + label, + sort, + onSort, + width, +}: { + column: SortColumn; + label: string; + sort: SortState; + onSort: (column: SortColumn) => void; + width?: string; +}) { + const isActive = sort.column === column; + return ( + + + + ); +} + +const filterItemClassName = + "flex w-full items-center justify-between rounded-sm px-1.5 py-1 text-left text-[13px] text-gray-12 transition-colors hover:bg-gray-3"; + +function RepositoryFilterHeader({ + repos, + selectedRepo, + onSelect, +}: { + repos: string[]; + selectedRepo: string | null; + onSelect: (repo: string | null) => void; +}) { + return ( + + + + + + + + + {repos.map((repo) => ( + + ))} + + + + + ); +} + interface BranchNotFoundPrompt { taskId: string; branchName: string; @@ -98,7 +223,7 @@ export interface ArchivedTasksViewPresentationProps { isLoading: boolean; branchNotFound: BranchNotFoundPrompt | null; onUnarchive: (taskId: string) => void; - onDelete: (taskId: string, taskTitle: string) => void; + onDelete: (taskId: string) => void; onContextMenu: (item: ArchivedTaskWithDetails, e: React.MouseEvent) => void; onBranchNotFoundClose: () => void; onRecreateBranch: () => void; @@ -114,9 +239,92 @@ export function ArchivedTasksViewPresentation({ onBranchNotFoundClose, onRecreateBranch, }: ArchivedTasksViewPresentationProps) { + const [searchQuery, setSearchQuery] = useState(""); + const [sort, setSort] = useState({ + column: "archived", + direction: "desc", + }); + const [repoFilter, setRepoFilter] = useState(null); + const [deleteTargetId, setDeleteTargetId] = useState(null); + + const handleSort = (column: SortColumn) => { + setSort((prev) => + prev.column === column + ? { column, direction: prev.direction === "asc" ? "desc" : "asc" } + : { column, direction: "desc" }, + ); + }; + + const itemsWithRepo = useMemo( + () => + items.map((item) => ({ + ...item, + repoName: getRepoName(item.task?.repository), + })), + [items], + ); + + const uniqueRepos = useMemo(() => { + const repos = new Set(); + for (const item of itemsWithRepo) { + if (item.repoName !== "—") repos.add(item.repoName); + } + return [...repos].sort((a, b) => a.localeCompare(b)); + }, [itemsWithRepo]); + + const filteredItems = useMemo(() => { + let result = itemsWithRepo; + + const query = searchQuery.trim().toLowerCase(); + if (query) { + result = result.filter((item) => + (item.task?.title?.toLowerCase() ?? "").includes(query), + ); + } + + if (repoFilter) { + result = result.filter((item) => item.repoName === repoFilter); + } + + const dir = sort.direction === "asc" ? 1 : -1; + + return [...result].sort((a, b) => { + const aTime = + sort.column === "created" + ? a.task?.created_at + ? new Date(a.task.created_at).getTime() + : 0 + : new Date(a.archived.archivedAt).getTime(); + const bTime = + sort.column === "created" + ? b.task?.created_at + ? new Date(b.task.created_at).getTime() + : 0 + : new Date(b.archived.archivedAt).getTime(); + return dir * (aTime - bTime); + }); + }, [itemsWithRepo, searchQuery, repoFilter, sort]); + return ( - + + + setSearchQuery(e.target.value)} + className="text-[13px]" + > + + + + + + {isLoading ? ( @@ -124,31 +332,49 @@ export function ArchivedTasksViewPresentation({ Loading archived tasks... - ) : items.length === 0 ? ( + ) : filteredItems.length === 0 ? ( - No archived tasks + + {items.length === 0 ? "No archived tasks" : "No matching tasks"} + ) : ( - + Title - - Created - - - Repository - - + + + + - {items.map((item) => ( + {filteredItems.map((item) => ( onContextMenu(item, e)} @@ -157,7 +383,7 @@ export function ArchivedTasksViewPresentation({ - + {item.task?.title ?? "Unknown task"} @@ -168,11 +394,16 @@ export function ArchivedTasksViewPresentation({ - - {getRepoName(item.task?.repository)} + + {formatRelativeDate(item.archived.archivedAt)} + + {item.repoName} + + + @@ -233,6 +459,47 @@ export function ArchivedTasksViewPresentation({ + + { + if (!open) setDeleteTargetId(null); + }} + > + + Delete archived task + + + Permanently delete{" "} + + {items.find((i) => i.archived.taskId === deleteTargetId)?.task + ?.title ?? "Unknown task"} + + ? This cannot be undone. + + + + + + + + + + + + ); } @@ -303,7 +570,7 @@ export function ArchivedTasksView() { const executeDelete = async (taskId: string) => { try { await trpcClient.archive.delete.mutate({ taskId }); - invalidateArchiveQueries(); + await invalidateArchiveQueries(); toast.success("Task deleted"); } catch (error) { const message = error instanceof Error ? error.message : String(error); @@ -311,16 +578,6 @@ export function ArchivedTasksView() { } }; - const handleDelete = async (taskId: string, taskTitle: string) => { - const { confirmed } = - await trpcClient.contextMenu.confirmDeleteArchivedTask.mutate({ - taskTitle, - }); - if (!confirmed) return; - - await executeDelete(taskId); - }; - const handleContextMenu = async ( item: ArchivedTaskWithDetails, e: React.MouseEvent, @@ -387,7 +644,7 @@ export function ArchivedTasksView() { isLoading={isLoading} branchNotFound={branchNotFound} onUnarchive={handleUnarchive} - onDelete={handleDelete} + onDelete={executeDelete} onContextMenu={handleContextMenu} onBranchNotFoundClose={() => setBranchNotFound(null)} onRecreateBranch={handleRecreateBranch} diff --git a/apps/code/src/renderer/styles/globals.css b/apps/code/src/renderer/styles/globals.css index ae2b62c0f..a9c0ca22a 100644 --- a/apps/code/src/renderer/styles/globals.css +++ b/apps/code/src/renderer/styles/globals.css @@ -159,6 +159,14 @@ } .radix-themes { + --cursor-button: pointer; + --cursor-checkbox: pointer; + --cursor-menu-item: pointer; + --cursor-radio: pointer; + --cursor-switch: pointer; + --cursor-slider-thumb: pointer; + --cursor-slider-thumb-active: grab; + /* Font families */ --default-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial,