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 <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dotta
2026-03-14 17:47:53 -05:00
parent c94132bc7e
commit aea133ff9f
6 changed files with 83 additions and 9 deletions

View File

@@ -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);

View File

@@ -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,
});

View File

@@ -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,
});

View File

@@ -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<string, unknown>) => void;
onFieldUpdate?: (field: ProjectConfigFieldKey, data: Record<string, unknown>) => 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
)}
</div>
{onArchive && (
<>
<Separator className="my-4" />
<div className="space-y-4 py-4">
<div className="text-xs font-medium text-destructive uppercase tracking-wide">
Danger Zone
</div>
<div className="space-y-3 rounded-md border border-destructive/40 bg-destructive/5 px-4 py-4">
<p className="text-sm text-muted-foreground">
{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."}
</p>
<Button
size="sm"
variant="destructive"
disabled={archivePending}
onClick={() => {
const action = project.archivedAt ? "Unarchive" : "Archive";
const confirmed = window.confirm(
`${action} project "${project.name}"?`,
);
if (!confirmed) return;
onArchive(!project.archivedAt);
}}
>
{archivePending ? (
<><Loader2 className="h-3 w-3 animate-spin mr-1" />{project.archivedAt ? "Unarchiving..." : "Archiving..."}</>
) : project.archivedAt ? (
<><ArchiveRestore className="h-3 w-3 mr-1" />Unarchive project</>
) : (
<><Archive className="h-3 w-3 mr-1" />Archive project</>
)}
</Button>
</div>
</div>
</>
)}
</div>
);
}

View File

@@ -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}
/>
</div>
)}

View File

@@ -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 <EmptyState icon={Hexagon} message="Select a company to view projects." />;
@@ -47,7 +51,7 @@ export function Projects() {
{error && <p className="text-sm text-destructive">{error.message}</p>}
{projects && projects.length === 0 && (
{!isLoading && projects.length === 0 && (
<EmptyState
icon={Hexagon}
message="No projects yet."
@@ -56,7 +60,7 @@ export function Projects() {
/>
)}
{projects && projects.length > 0 && (
{projects.length > 0 && (
<div className="border border-border">
{projects.map((project) => (
<EntityRow