Expand NewIssueDialog with richer form fields. Add NewProjectDialog. Enhance CommandPalette with more actions and search. Improve CompanySwitcher, EmptyState, and IssueProperties. Flesh out Activity, Companies, Dashboard, and Inbox pages with real content and layouts. Refine sidebar, routing, and dialog context. CSS tweaks for dark theme. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
202 lines
6.2 KiB
TypeScript
202 lines
6.2 KiB
TypeScript
import { useState, useEffect, useCallback } from "react";
|
|
import { useNavigate } from "react-router-dom";
|
|
import { useCompany } from "../context/CompanyContext";
|
|
import { useDialog } from "../context/DialogContext";
|
|
import { issuesApi } from "../api/issues";
|
|
import { agentsApi } from "../api/agents";
|
|
import { projectsApi } from "../api/projects";
|
|
import {
|
|
CommandDialog,
|
|
CommandEmpty,
|
|
CommandGroup,
|
|
CommandInput,
|
|
CommandItem,
|
|
CommandList,
|
|
CommandSeparator,
|
|
} from "@/components/ui/command";
|
|
import {
|
|
CircleDot,
|
|
Bot,
|
|
Hexagon,
|
|
Target,
|
|
LayoutDashboard,
|
|
Inbox,
|
|
DollarSign,
|
|
History,
|
|
GitBranch,
|
|
SquarePen,
|
|
Plus,
|
|
} from "lucide-react";
|
|
import type { Issue, Agent, Project } from "@paperclip/shared";
|
|
|
|
export function CommandPalette() {
|
|
const [open, setOpen] = useState(false);
|
|
const [issues, setIssues] = useState<Issue[]>([]);
|
|
const [agents, setAgents] = useState<Agent[]>([]);
|
|
const [projects, setProjects] = useState<Project[]>([]);
|
|
const navigate = useNavigate();
|
|
const { selectedCompanyId } = useCompany();
|
|
const { openNewIssue } = useDialog();
|
|
|
|
useEffect(() => {
|
|
function handleKeyDown(e: KeyboardEvent) {
|
|
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
|
|
e.preventDefault();
|
|
setOpen(true);
|
|
}
|
|
}
|
|
document.addEventListener("keydown", handleKeyDown);
|
|
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
}, []);
|
|
|
|
const loadData = useCallback(async () => {
|
|
if (!selectedCompanyId) return;
|
|
const [i, a, p] = await Promise.all([
|
|
issuesApi.list(selectedCompanyId).catch(() => []),
|
|
agentsApi.list(selectedCompanyId).catch(() => []),
|
|
projectsApi.list(selectedCompanyId).catch(() => []),
|
|
]);
|
|
setIssues(i);
|
|
setAgents(a);
|
|
setProjects(p);
|
|
}, [selectedCompanyId]);
|
|
|
|
useEffect(() => {
|
|
if (open) {
|
|
void loadData();
|
|
}
|
|
}, [open, loadData]);
|
|
|
|
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;
|
|
};
|
|
|
|
return (
|
|
<CommandDialog open={open} onOpenChange={setOpen}>
|
|
<CommandInput placeholder="Search issues, agents, projects..." />
|
|
<CommandList>
|
|
<CommandEmpty>No results found.</CommandEmpty>
|
|
|
|
<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>
|
|
<CommandItem onSelect={() => go("/org")}>
|
|
<GitBranch className="mr-2 h-4 w-4" />
|
|
Org Chart
|
|
</CommandItem>
|
|
</CommandGroup>
|
|
|
|
<CommandSeparator />
|
|
|
|
<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={() => go("/agents")}>
|
|
<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>
|
|
|
|
{issues.length > 0 && (
|
|
<>
|
|
<CommandSeparator />
|
|
<CommandGroup heading="Issues">
|
|
{issues.slice(0, 10).map((issue) => (
|
|
<CommandItem key={issue.id} onSelect={() => go(`/issues/${issue.id}`)}>
|
|
<CircleDot className="mr-2 h-4 w-4" />
|
|
<span className="text-muted-foreground mr-2 font-mono text-xs">
|
|
{issue.id.slice(0, 8)}
|
|
</span>
|
|
<span className="flex-1 truncate">{issue.title}</span>
|
|
{issue.assigneeAgentId && (
|
|
<span className="text-xs text-muted-foreground ml-2">
|
|
{agentName(issue.assigneeAgentId)}
|
|
</span>
|
|
)}
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</>
|
|
)}
|
|
|
|
{agents.length > 0 && (
|
|
<>
|
|
<CommandSeparator />
|
|
<CommandGroup heading="Agents">
|
|
{agents.slice(0, 10).map((agent) => (
|
|
<CommandItem key={agent.id} onSelect={() => go(`/agents/${agent.id}`)}>
|
|
<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(`/projects/${project.id}`)}>
|
|
<Hexagon className="mr-2 h-4 w-4" />
|
|
{project.name}
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</>
|
|
)}
|
|
</CommandList>
|
|
</CommandDialog>
|
|
);
|
|
}
|