From aea133ff9fe23d5b06a4d76ba0cfef8893f56927 Mon Sep 17 00:00:00 2001 From: Dotta Date: Sat, 14 Mar 2026 17:47:53 -0500 Subject: [PATCH] Add archive project button and filter archived projects from selectors - Add "Archive project" / "Unarchive project" button in the project configuration danger zone (ProjectProperties) - Filter archived projects from the Projects listing page - Filter archived projects from NewIssueDialog project selector - Filter archived projects from IssueProperties project picker (keeps current project visible even if archived) - Filter archived projects from CommandPalette - SidebarProjects already filters archived projects Co-Authored-By: Paperclip Co-Authored-By: Claude Opus 4.6 --- ui/src/components/CommandPalette.tsx | 6 +++- ui/src/components/IssueProperties.tsx | 6 +++- ui/src/components/NewIssueDialog.tsx | 6 +++- ui/src/components/ProjectProperties.tsx | 45 +++++++++++++++++++++++-- ui/src/pages/ProjectDetail.tsx | 17 ++++++++++ ui/src/pages/Projects.tsx | 12 ++++--- 6 files changed, 83 insertions(+), 9 deletions(-) diff --git a/ui/src/components/CommandPalette.tsx b/ui/src/components/CommandPalette.tsx index 3defb0e6..9d84be52 100644 --- a/ui/src/components/CommandPalette.tsx +++ b/ui/src/components/CommandPalette.tsx @@ -75,11 +75,15 @@ export function CommandPalette() { enabled: !!selectedCompanyId && open, }); - const { data: projects = [] } = useQuery({ + const { data: allProjects = [] } = useQuery({ queryKey: queryKeys.projects.list(selectedCompanyId!), queryFn: () => projectsApi.list(selectedCompanyId!), enabled: !!selectedCompanyId && open, }); + const projects = useMemo( + () => allProjects.filter((p) => !p.archivedAt), + [allProjects], + ); function go(path: string) { setOpen(false); diff --git a/ui/src/components/IssueProperties.tsx b/ui/src/components/IssueProperties.tsx index cf4b6a43..4781aea5 100644 --- a/ui/src/components/IssueProperties.tsx +++ b/ui/src/components/IssueProperties.tsx @@ -131,8 +131,12 @@ export function IssueProperties({ issue, onUpdate, inline }: IssuePropertiesProp queryFn: () => projectsApi.list(companyId!), enabled: !!companyId, }); + const activeProjects = useMemo( + () => (projects ?? []).filter((p) => !p.archivedAt || p.id === issue.projectId), + [projects, issue.projectId], + ); const { orderedProjects } = useProjectOrder({ - projects: projects ?? [], + projects: activeProjects, companyId, userId: currentUserId, }); diff --git a/ui/src/components/NewIssueDialog.tsx b/ui/src/components/NewIssueDialog.tsx index dc2d73c7..5a9ce792 100644 --- a/ui/src/components/NewIssueDialog.tsx +++ b/ui/src/components/NewIssueDialog.tsx @@ -288,8 +288,12 @@ export function NewIssueDialog() { queryFn: () => authApi.getSession(), }); const currentUserId = session?.user?.id ?? session?.session?.userId ?? null; + const activeProjects = useMemo( + () => (projects ?? []).filter((p) => !p.archivedAt), + [projects], + ); const { orderedProjects } = useProjectOrder({ - projects: projects ?? [], + projects: activeProjects, companyId: effectiveCompanyId, userId: currentUserId, }); diff --git a/ui/src/components/ProjectProperties.tsx b/ui/src/components/ProjectProperties.tsx index 9237f5e3..38dc1a33 100644 --- a/ui/src/components/ProjectProperties.tsx +++ b/ui/src/components/ProjectProperties.tsx @@ -13,7 +13,7 @@ import { Separator } from "@/components/ui/separator"; import { Button } from "@/components/ui/button"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; -import { AlertCircle, Check, ExternalLink, Github, Loader2, Plus, Trash2, X } from "lucide-react"; +import { AlertCircle, Archive, ArchiveRestore, Check, ExternalLink, Github, Loader2, Plus, Trash2, X } from "lucide-react"; import { ChoosePathButton } from "./PathInstructionsModal"; import { DraftInput } from "./agent-config-primitives"; import { InlineEditor } from "./InlineEditor"; @@ -34,6 +34,8 @@ interface ProjectPropertiesProps { onUpdate?: (data: Record) => void; onFieldUpdate?: (field: ProjectConfigFieldKey, data: Record) => void; getFieldSaveState?: (field: ProjectConfigFieldKey) => ProjectFieldSaveState; + onArchive?: (archived: boolean) => void; + archivePending?: boolean; } export type ProjectFieldSaveState = "idle" | "saving" | "saved" | "error"; @@ -152,7 +154,7 @@ function ProjectStatusPicker({ status, onChange }: { status: string; onChange: ( ); } -export function ProjectProperties({ project, onUpdate, onFieldUpdate, getFieldSaveState }: ProjectPropertiesProps) { +export function ProjectProperties({ project, onUpdate, onFieldUpdate, getFieldSaveState, onArchive, archivePending }: ProjectPropertiesProps) { const { selectedCompanyId } = useCompany(); const queryClient = useQueryClient(); const [goalOpen, setGoalOpen] = useState(false); @@ -954,6 +956,45 @@ export function ProjectProperties({ project, onUpdate, onFieldUpdate, getFieldSa )} + + {onArchive && ( + <> + +
+
+ Danger Zone +
+
+

+ {project.archivedAt + ? "Unarchive this project to restore it in the sidebar and project selectors." + : "Archive this project to hide it from the sidebar and project selectors."} +

+ +
+
+ + )} ); } diff --git a/ui/src/pages/ProjectDetail.tsx b/ui/src/pages/ProjectDetail.tsx index 42bb5b86..5134c22b 100644 --- a/ui/src/pages/ProjectDetail.tsx +++ b/ui/src/pages/ProjectDetail.tsx @@ -274,6 +274,21 @@ export function ProjectDetail() { onSuccess: invalidateProject, }); + const archiveProject = useMutation({ + mutationFn: (archived: boolean) => + projectsApi.update( + projectLookupRef, + { archivedAt: archived ? new Date().toISOString() : null }, + resolvedCompanyId ?? lookupCompanyId, + ), + onSuccess: (_, archived) => { + invalidateProject(); + if (archived) { + navigate("/projects"); + } + }, + }); + const uploadImage = useMutation({ mutationFn: async (file: File) => { if (!resolvedCompanyId) throw new Error("No company selected"); @@ -476,6 +491,8 @@ export function ProjectDetail() { onUpdate={(data) => updateProject.mutate(data)} onFieldUpdate={updateProjectField} getFieldSaveState={(field) => fieldSaveStates[field] ?? "idle"} + onArchive={(archived) => archiveProject.mutate(archived)} + archivePending={archiveProject.isPending} /> )} diff --git a/ui/src/pages/Projects.tsx b/ui/src/pages/Projects.tsx index 6fe80ada..886a2b60 100644 --- a/ui/src/pages/Projects.tsx +++ b/ui/src/pages/Projects.tsx @@ -1,4 +1,4 @@ -import { useEffect } from "react"; +import { useEffect, useMemo } from "react"; import { useQuery } from "@tanstack/react-query"; import { projectsApi } from "../api/projects"; import { useCompany } from "../context/CompanyContext"; @@ -22,11 +22,15 @@ export function Projects() { setBreadcrumbs([{ label: "Projects" }]); }, [setBreadcrumbs]); - const { data: projects, isLoading, error } = useQuery({ + const { data: allProjects, isLoading, error } = useQuery({ queryKey: queryKeys.projects.list(selectedCompanyId!), queryFn: () => projectsApi.list(selectedCompanyId!), enabled: !!selectedCompanyId, }); + const projects = useMemo( + () => (allProjects ?? []).filter((p) => !p.archivedAt), + [allProjects], + ); if (!selectedCompanyId) { return ; @@ -47,7 +51,7 @@ export function Projects() { {error &&

{error.message}

} - {projects && projects.length === 0 && ( + {!isLoading && projects.length === 0 && ( )} - {projects && projects.length > 0 && ( + {projects.length > 0 && (
{projects.map((project) => (