- 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>
240 lines
7.6 KiB
TypeScript
240 lines
7.6 KiB
TypeScript
import { useState, useEffect, useMemo } from "react";
|
|
import { useNavigate } from "@/lib/router";
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import { useCompany } from "../context/CompanyContext";
|
|
import { useDialog } from "../context/DialogContext";
|
|
import { useSidebar } from "../context/SidebarContext";
|
|
import { issuesApi } from "../api/issues";
|
|
import { agentsApi } from "../api/agents";
|
|
import { projectsApi } from "../api/projects";
|
|
import { queryKeys } from "../lib/queryKeys";
|
|
import {
|
|
CommandDialog,
|
|
CommandEmpty,
|
|
CommandGroup,
|
|
CommandInput,
|
|
CommandItem,
|
|
CommandList,
|
|
CommandSeparator,
|
|
} from "@/components/ui/command";
|
|
import {
|
|
CircleDot,
|
|
Bot,
|
|
Hexagon,
|
|
Target,
|
|
LayoutDashboard,
|
|
Inbox,
|
|
DollarSign,
|
|
History,
|
|
SquarePen,
|
|
Plus,
|
|
} from "lucide-react";
|
|
import { Identity } from "./Identity";
|
|
import { agentUrl, projectUrl } from "../lib/utils";
|
|
|
|
export function CommandPalette() {
|
|
const [open, setOpen] = useState(false);
|
|
const [query, setQuery] = useState("");
|
|
const navigate = useNavigate();
|
|
const { selectedCompanyId } = useCompany();
|
|
const { openNewIssue, openNewAgent } = useDialog();
|
|
const { isMobile, setSidebarOpen } = useSidebar();
|
|
const searchQuery = query.trim();
|
|
|
|
useEffect(() => {
|
|
function handleKeyDown(e: KeyboardEvent) {
|
|
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
|
|
e.preventDefault();
|
|
setOpen(true);
|
|
if (isMobile) setSidebarOpen(false);
|
|
}
|
|
}
|
|
document.addEventListener("keydown", handleKeyDown);
|
|
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
}, [isMobile, setSidebarOpen]);
|
|
|
|
useEffect(() => {
|
|
if (!open) setQuery("");
|
|
}, [open]);
|
|
|
|
const { data: issues = [] } = useQuery({
|
|
queryKey: queryKeys.issues.list(selectedCompanyId!),
|
|
queryFn: () => issuesApi.list(selectedCompanyId!),
|
|
enabled: !!selectedCompanyId && open,
|
|
});
|
|
|
|
const { data: searchedIssues = [] } = useQuery({
|
|
queryKey: queryKeys.issues.search(selectedCompanyId!, searchQuery),
|
|
queryFn: () => issuesApi.list(selectedCompanyId!, { q: searchQuery }),
|
|
enabled: !!selectedCompanyId && open && searchQuery.length > 0,
|
|
});
|
|
|
|
const { data: agents = [] } = useQuery({
|
|
queryKey: queryKeys.agents.list(selectedCompanyId!),
|
|
queryFn: () => agentsApi.list(selectedCompanyId!),
|
|
enabled: !!selectedCompanyId && open,
|
|
});
|
|
|
|
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);
|
|
navigate(path);
|
|
}
|
|
|
|
const agentName = (id: string | null) => {
|
|
if (!id) return null;
|
|
return agents.find((a) => a.id === id)?.name ?? null;
|
|
};
|
|
|
|
const visibleIssues = useMemo(
|
|
() => (searchQuery.length > 0 ? searchedIssues : issues),
|
|
[issues, searchedIssues, searchQuery],
|
|
);
|
|
|
|
return (
|
|
<CommandDialog open={open} onOpenChange={(v) => {
|
|
setOpen(v);
|
|
if (v && isMobile) setSidebarOpen(false);
|
|
}}>
|
|
<CommandInput
|
|
placeholder="Search issues, agents, projects..."
|
|
value={query}
|
|
onValueChange={setQuery}
|
|
/>
|
|
<CommandList>
|
|
<CommandEmpty>No results found.</CommandEmpty>
|
|
|
|
<CommandGroup heading="Actions">
|
|
<CommandItem
|
|
onSelect={() => {
|
|
setOpen(false);
|
|
openNewIssue();
|
|
}}
|
|
>
|
|
<SquarePen className="mr-2 h-4 w-4" />
|
|
Create new issue
|
|
<span className="ml-auto text-xs text-muted-foreground">C</span>
|
|
</CommandItem>
|
|
<CommandItem
|
|
onSelect={() => {
|
|
setOpen(false);
|
|
openNewAgent();
|
|
}}
|
|
>
|
|
<Plus className="mr-2 h-4 w-4" />
|
|
Create new agent
|
|
</CommandItem>
|
|
<CommandItem onSelect={() => go("/projects")}>
|
|
<Plus className="mr-2 h-4 w-4" />
|
|
Create new project
|
|
</CommandItem>
|
|
</CommandGroup>
|
|
|
|
<CommandSeparator />
|
|
|
|
<CommandGroup heading="Pages">
|
|
<CommandItem onSelect={() => go("/dashboard")}>
|
|
<LayoutDashboard className="mr-2 h-4 w-4" />
|
|
Dashboard
|
|
</CommandItem>
|
|
<CommandItem onSelect={() => go("/inbox")}>
|
|
<Inbox className="mr-2 h-4 w-4" />
|
|
Inbox
|
|
</CommandItem>
|
|
<CommandItem onSelect={() => go("/issues")}>
|
|
<CircleDot className="mr-2 h-4 w-4" />
|
|
Issues
|
|
</CommandItem>
|
|
<CommandItem onSelect={() => go("/projects")}>
|
|
<Hexagon className="mr-2 h-4 w-4" />
|
|
Projects
|
|
</CommandItem>
|
|
<CommandItem onSelect={() => go("/goals")}>
|
|
<Target className="mr-2 h-4 w-4" />
|
|
Goals
|
|
</CommandItem>
|
|
<CommandItem onSelect={() => go("/agents")}>
|
|
<Bot className="mr-2 h-4 w-4" />
|
|
Agents
|
|
</CommandItem>
|
|
<CommandItem onSelect={() => go("/costs")}>
|
|
<DollarSign className="mr-2 h-4 w-4" />
|
|
Costs
|
|
</CommandItem>
|
|
<CommandItem onSelect={() => go("/activity")}>
|
|
<History className="mr-2 h-4 w-4" />
|
|
Activity
|
|
</CommandItem>
|
|
</CommandGroup>
|
|
|
|
{visibleIssues.length > 0 && (
|
|
<>
|
|
<CommandSeparator />
|
|
<CommandGroup heading="Issues">
|
|
{visibleIssues.slice(0, 10).map((issue) => (
|
|
<CommandItem
|
|
key={issue.id}
|
|
value={
|
|
searchQuery.length > 0
|
|
? `${searchQuery} ${issue.identifier ?? ""} ${issue.title}`
|
|
: undefined
|
|
}
|
|
onSelect={() => go(`/issues/${issue.identifier ?? issue.id}`)}
|
|
>
|
|
<CircleDot className="mr-2 h-4 w-4" />
|
|
<span className="text-muted-foreground mr-2 font-mono text-xs">
|
|
{issue.identifier ?? issue.id.slice(0, 8)}
|
|
</span>
|
|
<span className="flex-1 truncate">{issue.title}</span>
|
|
{issue.assigneeAgentId && (() => {
|
|
const name = agentName(issue.assigneeAgentId);
|
|
return name ? <Identity name={name} size="sm" className="ml-2 hidden sm:inline-flex" /> : null;
|
|
})()}
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</>
|
|
)}
|
|
|
|
{agents.length > 0 && (
|
|
<>
|
|
<CommandSeparator />
|
|
<CommandGroup heading="Agents">
|
|
{agents.slice(0, 10).map((agent) => (
|
|
<CommandItem key={agent.id} onSelect={() => go(agentUrl(agent))}>
|
|
<Bot className="mr-2 h-4 w-4" />
|
|
{agent.name}
|
|
<span className="text-xs text-muted-foreground ml-2">{agent.role}</span>
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</>
|
|
)}
|
|
|
|
{projects.length > 0 && (
|
|
<>
|
|
<CommandSeparator />
|
|
<CommandGroup heading="Projects">
|
|
{projects.slice(0, 10).map((project) => (
|
|
<CommandItem key={project.id} onSelect={() => go(projectUrl(project))}>
|
|
<Hexagon className="mr-2 h-4 w-4" />
|
|
{project.name}
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</>
|
|
)}
|
|
</CommandList>
|
|
</CommandDialog>
|
|
);
|
|
}
|