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:
@@ -75,11 +75,15 @@ export function CommandPalette() {
|
|||||||
enabled: !!selectedCompanyId && open,
|
enabled: !!selectedCompanyId && open,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: projects = [] } = useQuery({
|
const { data: allProjects = [] } = useQuery({
|
||||||
queryKey: queryKeys.projects.list(selectedCompanyId!),
|
queryKey: queryKeys.projects.list(selectedCompanyId!),
|
||||||
queryFn: () => projectsApi.list(selectedCompanyId!),
|
queryFn: () => projectsApi.list(selectedCompanyId!),
|
||||||
enabled: !!selectedCompanyId && open,
|
enabled: !!selectedCompanyId && open,
|
||||||
});
|
});
|
||||||
|
const projects = useMemo(
|
||||||
|
() => allProjects.filter((p) => !p.archivedAt),
|
||||||
|
[allProjects],
|
||||||
|
);
|
||||||
|
|
||||||
function go(path: string) {
|
function go(path: string) {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
|
|||||||
@@ -131,8 +131,12 @@ export function IssueProperties({ issue, onUpdate, inline }: IssuePropertiesProp
|
|||||||
queryFn: () => projectsApi.list(companyId!),
|
queryFn: () => projectsApi.list(companyId!),
|
||||||
enabled: !!companyId,
|
enabled: !!companyId,
|
||||||
});
|
});
|
||||||
|
const activeProjects = useMemo(
|
||||||
|
() => (projects ?? []).filter((p) => !p.archivedAt || p.id === issue.projectId),
|
||||||
|
[projects, issue.projectId],
|
||||||
|
);
|
||||||
const { orderedProjects } = useProjectOrder({
|
const { orderedProjects } = useProjectOrder({
|
||||||
projects: projects ?? [],
|
projects: activeProjects,
|
||||||
companyId,
|
companyId,
|
||||||
userId: currentUserId,
|
userId: currentUserId,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -288,8 +288,12 @@ export function NewIssueDialog() {
|
|||||||
queryFn: () => authApi.getSession(),
|
queryFn: () => authApi.getSession(),
|
||||||
});
|
});
|
||||||
const currentUserId = session?.user?.id ?? session?.session?.userId ?? null;
|
const currentUserId = session?.user?.id ?? session?.session?.userId ?? null;
|
||||||
|
const activeProjects = useMemo(
|
||||||
|
() => (projects ?? []).filter((p) => !p.archivedAt),
|
||||||
|
[projects],
|
||||||
|
);
|
||||||
const { orderedProjects } = useProjectOrder({
|
const { orderedProjects } = useProjectOrder({
|
||||||
projects: projects ?? [],
|
projects: activeProjects,
|
||||||
companyId: effectiveCompanyId,
|
companyId: effectiveCompanyId,
|
||||||
userId: currentUserId,
|
userId: currentUserId,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { Separator } from "@/components/ui/separator";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
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 { ChoosePathButton } from "./PathInstructionsModal";
|
||||||
import { DraftInput } from "./agent-config-primitives";
|
import { DraftInput } from "./agent-config-primitives";
|
||||||
import { InlineEditor } from "./InlineEditor";
|
import { InlineEditor } from "./InlineEditor";
|
||||||
@@ -34,6 +34,8 @@ interface ProjectPropertiesProps {
|
|||||||
onUpdate?: (data: Record<string, unknown>) => void;
|
onUpdate?: (data: Record<string, unknown>) => void;
|
||||||
onFieldUpdate?: (field: ProjectConfigFieldKey, data: Record<string, unknown>) => void;
|
onFieldUpdate?: (field: ProjectConfigFieldKey, data: Record<string, unknown>) => void;
|
||||||
getFieldSaveState?: (field: ProjectConfigFieldKey) => ProjectFieldSaveState;
|
getFieldSaveState?: (field: ProjectConfigFieldKey) => ProjectFieldSaveState;
|
||||||
|
onArchive?: (archived: boolean) => void;
|
||||||
|
archivePending?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ProjectFieldSaveState = "idle" | "saving" | "saved" | "error";
|
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 { selectedCompanyId } = useCompany();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [goalOpen, setGoalOpen] = useState(false);
|
const [goalOpen, setGoalOpen] = useState(false);
|
||||||
@@ -954,6 +956,45 @@ export function ProjectProperties({ project, onUpdate, onFieldUpdate, getFieldSa
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
</div>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -274,6 +274,21 @@ export function ProjectDetail() {
|
|||||||
onSuccess: invalidateProject,
|
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({
|
const uploadImage = useMutation({
|
||||||
mutationFn: async (file: File) => {
|
mutationFn: async (file: File) => {
|
||||||
if (!resolvedCompanyId) throw new Error("No company selected");
|
if (!resolvedCompanyId) throw new Error("No company selected");
|
||||||
@@ -476,6 +491,8 @@ export function ProjectDetail() {
|
|||||||
onUpdate={(data) => updateProject.mutate(data)}
|
onUpdate={(data) => updateProject.mutate(data)}
|
||||||
onFieldUpdate={updateProjectField}
|
onFieldUpdate={updateProjectField}
|
||||||
getFieldSaveState={(field) => fieldSaveStates[field] ?? "idle"}
|
getFieldSaveState={(field) => fieldSaveStates[field] ?? "idle"}
|
||||||
|
onArchive={(archived) => archiveProject.mutate(archived)}
|
||||||
|
archivePending={archiveProject.isPending}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect, useMemo } from "react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { projectsApi } from "../api/projects";
|
import { projectsApi } from "../api/projects";
|
||||||
import { useCompany } from "../context/CompanyContext";
|
import { useCompany } from "../context/CompanyContext";
|
||||||
@@ -22,11 +22,15 @@ export function Projects() {
|
|||||||
setBreadcrumbs([{ label: "Projects" }]);
|
setBreadcrumbs([{ label: "Projects" }]);
|
||||||
}, [setBreadcrumbs]);
|
}, [setBreadcrumbs]);
|
||||||
|
|
||||||
const { data: projects, isLoading, error } = useQuery({
|
const { data: allProjects, isLoading, error } = useQuery({
|
||||||
queryKey: queryKeys.projects.list(selectedCompanyId!),
|
queryKey: queryKeys.projects.list(selectedCompanyId!),
|
||||||
queryFn: () => projectsApi.list(selectedCompanyId!),
|
queryFn: () => projectsApi.list(selectedCompanyId!),
|
||||||
enabled: !!selectedCompanyId,
|
enabled: !!selectedCompanyId,
|
||||||
});
|
});
|
||||||
|
const projects = useMemo(
|
||||||
|
() => (allProjects ?? []).filter((p) => !p.archivedAt),
|
||||||
|
[allProjects],
|
||||||
|
);
|
||||||
|
|
||||||
if (!selectedCompanyId) {
|
if (!selectedCompanyId) {
|
||||||
return <EmptyState icon={Hexagon} message="Select a company to view projects." />;
|
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>}
|
{error && <p className="text-sm text-destructive">{error.message}</p>}
|
||||||
|
|
||||||
{projects && projects.length === 0 && (
|
{!isLoading && projects.length === 0 && (
|
||||||
<EmptyState
|
<EmptyState
|
||||||
icon={Hexagon}
|
icon={Hexagon}
|
||||||
message="No projects yet."
|
message="No projects yet."
|
||||||
@@ -56,7 +60,7 @@ export function Projects() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{projects && projects.length > 0 && (
|
{projects.length > 0 && (
|
||||||
<div className="border border-border">
|
<div className="border border-border">
|
||||||
{projects.map((project) => (
|
{projects.map((project) => (
|
||||||
<EntityRow
|
<EntityRow
|
||||||
|
|||||||
Reference in New Issue
Block a user