Replace custom useApi/useAgents hooks with @tanstack/react-query. Add LiveUpdatesProvider for WebSocket-driven cache invalidation. Add queryKeys module for centralized cache key management. Rework all pages and dialogs to use React Query mutations and queries. Improve CompanyContext with query-based data fetching. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
103 lines
3.3 KiB
TypeScript
103 lines
3.3 KiB
TypeScript
import type { Issue } from "@paperclip/shared";
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import { agentsApi } from "../api/agents";
|
|
import { useCompany } from "../context/CompanyContext";
|
|
import { queryKeys } from "../lib/queryKeys";
|
|
import { StatusIcon } from "./StatusIcon";
|
|
import { PriorityIcon } from "./PriorityIcon";
|
|
import { formatDate } from "../lib/utils";
|
|
import { timeAgo } from "../lib/timeAgo";
|
|
import { Separator } from "@/components/ui/separator";
|
|
|
|
interface IssuePropertiesProps {
|
|
issue: Issue;
|
|
onUpdate: (data: Record<string, unknown>) => void;
|
|
}
|
|
|
|
function PropertyRow({ label, children }: { label: string; children: React.ReactNode }) {
|
|
return (
|
|
<div className="flex items-center justify-between py-1.5">
|
|
<span className="text-xs text-muted-foreground">{label}</span>
|
|
<div className="flex items-center gap-1.5">{children}</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function statusLabel(status: string): string {
|
|
return status.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
}
|
|
|
|
function priorityLabel(priority: string): string {
|
|
return priority.charAt(0).toUpperCase() + priority.slice(1);
|
|
}
|
|
|
|
export function IssueProperties({ issue, onUpdate }: IssuePropertiesProps) {
|
|
const { selectedCompanyId } = useCompany();
|
|
const { data: agents } = useQuery({
|
|
queryKey: queryKeys.agents.list(selectedCompanyId!),
|
|
queryFn: () => agentsApi.list(selectedCompanyId!),
|
|
enabled: !!selectedCompanyId,
|
|
});
|
|
|
|
const agentName = (id: string | null) => {
|
|
if (!id || !agents) return null;
|
|
const agent = agents.find((a) => a.id === id);
|
|
return agent?.name ?? id.slice(0, 8);
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<div className="space-y-1">
|
|
<PropertyRow label="Status">
|
|
<StatusIcon
|
|
status={issue.status}
|
|
onChange={(status) => onUpdate({ status })}
|
|
/>
|
|
<span className="text-sm">{statusLabel(issue.status)}</span>
|
|
</PropertyRow>
|
|
|
|
<PropertyRow label="Priority">
|
|
<PriorityIcon
|
|
priority={issue.priority}
|
|
onChange={(priority) => onUpdate({ priority })}
|
|
/>
|
|
<span className="text-sm">{priorityLabel(issue.priority)}</span>
|
|
</PropertyRow>
|
|
|
|
<PropertyRow label="Assignee">
|
|
<span className="text-sm">
|
|
{issue.assigneeAgentId ? agentName(issue.assigneeAgentId) : "Unassigned"}
|
|
</span>
|
|
</PropertyRow>
|
|
|
|
<PropertyRow label="Project">
|
|
<span className="text-sm text-muted-foreground">
|
|
{issue.projectId ? issue.projectId.slice(0, 8) : "None"}
|
|
</span>
|
|
</PropertyRow>
|
|
</div>
|
|
|
|
<Separator />
|
|
|
|
<div className="space-y-1">
|
|
{issue.startedAt && (
|
|
<PropertyRow label="Started">
|
|
<span className="text-sm">{formatDate(issue.startedAt)}</span>
|
|
</PropertyRow>
|
|
)}
|
|
{issue.completedAt && (
|
|
<PropertyRow label="Completed">
|
|
<span className="text-sm">{formatDate(issue.completedAt)}</span>
|
|
</PropertyRow>
|
|
)}
|
|
<PropertyRow label="Created">
|
|
<span className="text-sm">{formatDate(issue.createdAt)}</span>
|
|
</PropertyRow>
|
|
<PropertyRow label="Updated">
|
|
<span className="text-sm">{timeAgo(issue.updatedAt)}</span>
|
|
</PropertyRow>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|