Adopt React Query and live updates across all UI pages
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>
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { activityApi } from "../api/activity";
|
||||
import { useCompany } from "../context/CompanyContext";
|
||||
import { useBreadcrumbs } from "../context/BreadcrumbContext";
|
||||
import { useApi } from "../hooks/useApi";
|
||||
import { queryKeys } from "../lib/queryKeys";
|
||||
import { EmptyState } from "../components/EmptyState";
|
||||
import { timeAgo } from "../lib/timeAgo";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
@@ -17,7 +18,6 @@ import {
|
||||
import { History, Bot, User, Settings } from "lucide-react";
|
||||
|
||||
function formatAction(action: string, entityType: string, entityId: string): string {
|
||||
const shortId = entityId.slice(0, 8);
|
||||
const actionMap: Record<string, string> = {
|
||||
"company.created": "Company created",
|
||||
"agent.created": `Agent created`,
|
||||
@@ -80,12 +80,11 @@ export function Activity() {
|
||||
setBreadcrumbs([{ label: "Activity" }]);
|
||||
}, [setBreadcrumbs]);
|
||||
|
||||
const fetcher = useCallback(() => {
|
||||
if (!selectedCompanyId) return Promise.resolve([]);
|
||||
return activityApi.list(selectedCompanyId);
|
||||
}, [selectedCompanyId]);
|
||||
|
||||
const { data, loading, error } = useApi(fetcher);
|
||||
const { data, isLoading, error } = useQuery({
|
||||
queryKey: queryKeys.activity(selectedCompanyId!),
|
||||
queryFn: () => activityApi.list(selectedCompanyId!),
|
||||
enabled: !!selectedCompanyId,
|
||||
});
|
||||
|
||||
if (!selectedCompanyId) {
|
||||
return <EmptyState icon={History} message="Select a company to view activity." />;
|
||||
@@ -119,7 +118,7 @@ export function Activity() {
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{loading && <p className="text-sm text-muted-foreground">Loading...</p>}
|
||||
{isLoading && <p className="text-sm text-muted-foreground">Loading...</p>}
|
||||
{error && <p className="text-sm text-destructive">{error.message}</p>}
|
||||
|
||||
{filtered && filtered.length === 0 && (
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { agentsApi } from "../api/agents";
|
||||
import { heartbeatsApi } from "../api/heartbeats";
|
||||
import { issuesApi } from "../api/issues";
|
||||
import { useApi } from "../hooks/useApi";
|
||||
import { usePanel } from "../context/PanelContext";
|
||||
import { useCompany } from "../context/CompanyContext";
|
||||
import { useBreadcrumbs } from "../context/BreadcrumbContext";
|
||||
import { queryKeys } from "../lib/queryKeys";
|
||||
import { AgentProperties } from "../components/AgentProperties";
|
||||
import { StatusBadge } from "../components/StatusBadge";
|
||||
import { EntityRow } from "../components/EntityRow";
|
||||
@@ -20,29 +21,53 @@ export function AgentDetail() {
|
||||
const { selectedCompanyId } = useCompany();
|
||||
const { openPanel, closePanel } = usePanel();
|
||||
const { setBreadcrumbs } = useBreadcrumbs();
|
||||
const queryClient = useQueryClient();
|
||||
const [actionError, setActionError] = useState<string | null>(null);
|
||||
|
||||
const agentFetcher = useCallback(() => {
|
||||
if (!agentId) return Promise.reject(new Error("No agent ID"));
|
||||
return agentsApi.get(agentId);
|
||||
}, [agentId]);
|
||||
const { data: agent, isLoading, error } = useQuery({
|
||||
queryKey: queryKeys.agents.detail(agentId!),
|
||||
queryFn: () => agentsApi.get(agentId!),
|
||||
enabled: !!agentId,
|
||||
});
|
||||
|
||||
const heartbeatsFetcher = useCallback(() => {
|
||||
if (!selectedCompanyId || !agentId) return Promise.resolve([] as HeartbeatRun[]);
|
||||
return heartbeatsApi.list(selectedCompanyId, agentId);
|
||||
}, [selectedCompanyId, agentId]);
|
||||
const { data: heartbeats } = useQuery({
|
||||
queryKey: queryKeys.heartbeats(selectedCompanyId!, agentId),
|
||||
queryFn: () => heartbeatsApi.list(selectedCompanyId!, agentId),
|
||||
enabled: !!selectedCompanyId && !!agentId,
|
||||
});
|
||||
|
||||
const issuesFetcher = useCallback(() => {
|
||||
if (!selectedCompanyId) return Promise.resolve([] as Issue[]);
|
||||
return issuesApi.list(selectedCompanyId);
|
||||
}, [selectedCompanyId]);
|
||||
|
||||
const { data: agent, loading, error, reload: reloadAgent } = useApi(agentFetcher);
|
||||
const { data: heartbeats } = useApi(heartbeatsFetcher);
|
||||
const { data: allIssues } = useApi(issuesFetcher);
|
||||
const { data: allIssues } = useQuery({
|
||||
queryKey: queryKeys.issues.list(selectedCompanyId!),
|
||||
queryFn: () => issuesApi.list(selectedCompanyId!),
|
||||
enabled: !!selectedCompanyId,
|
||||
});
|
||||
|
||||
const assignedIssues = (allIssues ?? []).filter((i) => i.assigneeAgentId === agentId);
|
||||
|
||||
const agentAction = useMutation({
|
||||
mutationFn: async (action: "invoke" | "pause" | "resume") => {
|
||||
if (!agentId) return Promise.reject(new Error("No agent ID"));
|
||||
if (action === "invoke") {
|
||||
await agentsApi.invoke(agentId);
|
||||
return;
|
||||
}
|
||||
if (action === "pause") {
|
||||
await agentsApi.pause(agentId);
|
||||
return;
|
||||
}
|
||||
await agentsApi.resume(agentId);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(agentId!) });
|
||||
if (selectedCompanyId) {
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.agents.list(selectedCompanyId) });
|
||||
}
|
||||
},
|
||||
onError: (err) => {
|
||||
setActionError(err instanceof Error ? err.message : "Action failed");
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setBreadcrumbs([
|
||||
{ label: "Agents", href: "/agents" },
|
||||
@@ -57,20 +82,7 @@ export function AgentDetail() {
|
||||
return () => closePanel();
|
||||
}, [agent]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
async function handleAction(action: "invoke" | "pause" | "resume") {
|
||||
if (!agentId) return;
|
||||
setActionError(null);
|
||||
try {
|
||||
if (action === "invoke") await agentsApi.invoke(agentId);
|
||||
else if (action === "pause") await agentsApi.pause(agentId);
|
||||
else await agentsApi.resume(agentId);
|
||||
reloadAgent();
|
||||
} catch (err) {
|
||||
setActionError(err instanceof Error ? err.message : `Failed to ${action} agent`);
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) return <p className="text-sm text-muted-foreground">Loading...</p>;
|
||||
if (isLoading) return <p className="text-sm text-muted-foreground">Loading...</p>;
|
||||
if (error) return <p className="text-sm text-destructive">{error.message}</p>;
|
||||
if (!agent) return null;
|
||||
|
||||
@@ -89,15 +101,15 @@ export function AgentDetail() {
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" size="sm" onClick={() => handleAction("invoke")}>
|
||||
<Button variant="outline" size="sm" onClick={() => agentAction.mutate("invoke")}>
|
||||
Invoke
|
||||
</Button>
|
||||
{agent.status === "active" ? (
|
||||
<Button variant="outline" size="sm" onClick={() => handleAction("pause")}>
|
||||
<Button variant="outline" size="sm" onClick={() => agentAction.mutate("pause")}>
|
||||
Pause
|
||||
</Button>
|
||||
) : (
|
||||
<Button variant="outline" size="sm" onClick={() => handleAction("resume")}>
|
||||
<Button variant="outline" size="sm" onClick={() => agentAction.mutate("resume")}>
|
||||
Resume
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useAgents } from "../hooks/useAgents";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { agentsApi } from "../api/agents";
|
||||
import { useCompany } from "../context/CompanyContext";
|
||||
import { useBreadcrumbs } from "../context/BreadcrumbContext";
|
||||
import { agentsApi } from "../api/agents";
|
||||
import { queryKeys } from "../lib/queryKeys";
|
||||
import { StatusBadge } from "../components/StatusBadge";
|
||||
import { EntityRow } from "../components/EntityRow";
|
||||
import { EmptyState } from "../components/EmptyState";
|
||||
@@ -12,10 +13,14 @@ import { Bot } from "lucide-react";
|
||||
|
||||
export function Agents() {
|
||||
const { selectedCompanyId } = useCompany();
|
||||
const { data: agents, loading, error, reload } = useAgents(selectedCompanyId);
|
||||
const { setBreadcrumbs } = useBreadcrumbs();
|
||||
const navigate = useNavigate();
|
||||
const [actionError, setActionError] = useState<string | null>(null);
|
||||
|
||||
const { data: agents, isLoading, error } = useQuery({
|
||||
queryKey: queryKeys.agents.list(selectedCompanyId!),
|
||||
queryFn: () => agentsApi.list(selectedCompanyId!),
|
||||
enabled: !!selectedCompanyId,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setBreadcrumbs([{ label: "Agents" }]);
|
||||
@@ -29,9 +34,8 @@ export function Agents() {
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-lg font-semibold">Agents</h2>
|
||||
|
||||
{loading && <p className="text-sm text-muted-foreground">Loading...</p>}
|
||||
{isLoading && <p className="text-sm text-muted-foreground">Loading...</p>}
|
||||
{error && <p className="text-sm text-destructive">{error.message}</p>}
|
||||
{actionError && <p className="text-sm text-destructive">{actionError}</p>}
|
||||
|
||||
{agents && agents.length === 0 && (
|
||||
<EmptyState
|
||||
|
||||
@@ -1,41 +1,42 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { approvalsApi } from "../api/approvals";
|
||||
import { useCompany } from "../context/CompanyContext";
|
||||
import { useApi } from "../hooks/useApi";
|
||||
import { queryKeys } from "../lib/queryKeys";
|
||||
import { StatusBadge } from "../components/StatusBadge";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
export function Approvals() {
|
||||
const { selectedCompanyId } = useCompany();
|
||||
const queryClient = useQueryClient();
|
||||
const [actionError, setActionError] = useState<string | null>(null);
|
||||
|
||||
const fetcher = useCallback(() => {
|
||||
if (!selectedCompanyId) return Promise.resolve([]);
|
||||
return approvalsApi.list(selectedCompanyId);
|
||||
}, [selectedCompanyId]);
|
||||
const { data, isLoading, error } = useQuery({
|
||||
queryKey: queryKeys.approvals.list(selectedCompanyId!),
|
||||
queryFn: () => approvalsApi.list(selectedCompanyId!),
|
||||
enabled: !!selectedCompanyId,
|
||||
});
|
||||
|
||||
const { data, loading, error, reload } = useApi(fetcher);
|
||||
|
||||
async function approve(id: string) {
|
||||
setActionError(null);
|
||||
try {
|
||||
await approvalsApi.approve(id);
|
||||
reload();
|
||||
} catch (err) {
|
||||
const approveMutation = useMutation({
|
||||
mutationFn: (id: string) => approvalsApi.approve(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.approvals.list(selectedCompanyId!) });
|
||||
},
|
||||
onError: (err) => {
|
||||
setActionError(err instanceof Error ? err.message : "Failed to approve");
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
async function reject(id: string) {
|
||||
setActionError(null);
|
||||
try {
|
||||
await approvalsApi.reject(id);
|
||||
reload();
|
||||
} catch (err) {
|
||||
const rejectMutation = useMutation({
|
||||
mutationFn: (id: string) => approvalsApi.reject(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.approvals.list(selectedCompanyId!) });
|
||||
},
|
||||
onError: (err) => {
|
||||
setActionError(err instanceof Error ? err.message : "Failed to reject");
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if (!selectedCompanyId) {
|
||||
return <p className="text-muted-foreground">Select a company first.</p>;
|
||||
@@ -44,7 +45,7 @@ export function Approvals() {
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold mb-4">Approvals</h2>
|
||||
{loading && <p className="text-muted-foreground">Loading...</p>}
|
||||
{isLoading && <p className="text-muted-foreground">Loading...</p>}
|
||||
{error && <p className="text-destructive">{error.message}</p>}
|
||||
{actionError && <p className="text-destructive">{actionError}</p>}
|
||||
|
||||
@@ -68,14 +69,14 @@ export function Approvals() {
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="border-green-700 text-green-400 hover:bg-green-900/50"
|
||||
onClick={() => approve(approval.id)}
|
||||
onClick={() => approveMutation.mutate(approval.id)}
|
||||
>
|
||||
Approve
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={() => reject(approval.id)}
|
||||
onClick={() => rejectMutation.mutate(approval.id)}
|
||||
>
|
||||
Reject
|
||||
</Button>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { useCompany } from "../context/CompanyContext";
|
||||
import { useBreadcrumbs } from "../context/BreadcrumbContext";
|
||||
import { companiesApi } from "../api/companies";
|
||||
import { queryKeys } from "../lib/queryKeys";
|
||||
import { formatCents } from "../lib/utils";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
@@ -16,9 +18,9 @@ export function Companies() {
|
||||
createCompany,
|
||||
loading,
|
||||
error,
|
||||
reloadCompanies,
|
||||
} = useCompany();
|
||||
const { setBreadcrumbs } = useBreadcrumbs();
|
||||
const queryClient = useQueryClient();
|
||||
const [name, setName] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [budget, setBudget] = useState("0");
|
||||
@@ -28,7 +30,15 @@ export function Companies() {
|
||||
// Inline edit state
|
||||
const [editingId, setEditingId] = useState<string | null>(null);
|
||||
const [editName, setEditName] = useState("");
|
||||
const [editSaving, setEditSaving] = useState(false);
|
||||
|
||||
const editMutation = useMutation({
|
||||
mutationFn: ({ id, newName }: { id: string; newName: string }) =>
|
||||
companiesApi.update(id, { name: newName }),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.companies.all });
|
||||
setEditingId(null);
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setBreadcrumbs([{ label: "Companies" }]);
|
||||
@@ -49,7 +59,6 @@ export function Companies() {
|
||||
setName("");
|
||||
setDescription("");
|
||||
setBudget("0");
|
||||
await reloadCompanies();
|
||||
} catch (err) {
|
||||
setSubmitError(err instanceof Error ? err.message : "Failed to create company");
|
||||
} finally {
|
||||
@@ -62,16 +71,9 @@ export function Companies() {
|
||||
setEditName(currentName);
|
||||
}
|
||||
|
||||
async function saveEdit() {
|
||||
function saveEdit() {
|
||||
if (!editingId || !editName.trim()) return;
|
||||
setEditSaving(true);
|
||||
try {
|
||||
await companiesApi.update(editingId, { name: editName.trim() });
|
||||
await reloadCompanies();
|
||||
setEditingId(null);
|
||||
} finally {
|
||||
setEditSaving(false);
|
||||
}
|
||||
editMutation.mutate({ id: editingId, newName: editName.trim() });
|
||||
}
|
||||
|
||||
function cancelEdit() {
|
||||
@@ -151,7 +153,7 @@ export function Companies() {
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
onClick={saveEdit}
|
||||
disabled={editSaving}
|
||||
disabled={editMutation.isPending}
|
||||
>
|
||||
<Check className="h-3.5 w-3.5 text-green-500" />
|
||||
</Button>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { useEffect } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { costsApi } from "../api/costs";
|
||||
import { useCompany } from "../context/CompanyContext";
|
||||
import { useBreadcrumbs } from "../context/BreadcrumbContext";
|
||||
import { useApi } from "../hooks/useApi";
|
||||
import { queryKeys } from "../lib/queryKeys";
|
||||
import { EmptyState } from "../components/EmptyState";
|
||||
import { formatCents } from "../lib/utils";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
@@ -16,17 +17,18 @@ export function Costs() {
|
||||
setBreadcrumbs([{ label: "Costs" }]);
|
||||
}, [setBreadcrumbs]);
|
||||
|
||||
const fetcher = useCallback(async () => {
|
||||
if (!selectedCompanyId) return null;
|
||||
const [summary, byAgent, byProject] = await Promise.all([
|
||||
costsApi.summary(selectedCompanyId),
|
||||
costsApi.byAgent(selectedCompanyId),
|
||||
costsApi.byProject(selectedCompanyId),
|
||||
]);
|
||||
return { summary, byAgent, byProject };
|
||||
}, [selectedCompanyId]);
|
||||
|
||||
const { data, loading, error } = useApi(fetcher);
|
||||
const { data, isLoading, error } = useQuery({
|
||||
queryKey: queryKeys.costs(selectedCompanyId!),
|
||||
queryFn: async () => {
|
||||
const [summary, byAgent, byProject] = await Promise.all([
|
||||
costsApi.summary(selectedCompanyId!),
|
||||
costsApi.byAgent(selectedCompanyId!),
|
||||
costsApi.byProject(selectedCompanyId!),
|
||||
]);
|
||||
return { summary, byAgent, byProject };
|
||||
},
|
||||
enabled: !!selectedCompanyId,
|
||||
});
|
||||
|
||||
if (!selectedCompanyId) {
|
||||
return <EmptyState icon={DollarSign} message="Select a company to view costs." />;
|
||||
@@ -36,7 +38,7 @@ export function Costs() {
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-lg font-semibold">Costs</h2>
|
||||
|
||||
{loading && <p className="text-sm text-muted-foreground">Loading...</p>}
|
||||
{isLoading && <p className="text-sm text-muted-foreground">Loading...</p>}
|
||||
{error && <p className="text-sm text-destructive">{error.message}</p>}
|
||||
|
||||
{data && (
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { dashboardApi } from "../api/dashboard";
|
||||
import { activityApi } from "../api/activity";
|
||||
import { issuesApi } from "../api/issues";
|
||||
import { agentsApi } from "../api/agents";
|
||||
import { useCompany } from "../context/CompanyContext";
|
||||
import { useAgents } from "../hooks/useAgents";
|
||||
import { useBreadcrumbs } from "../context/BreadcrumbContext";
|
||||
import { useApi } from "../hooks/useApi";
|
||||
import { queryKeys } from "../lib/queryKeys";
|
||||
import { MetricCard } from "../components/MetricCard";
|
||||
import { EmptyState } from "../components/EmptyState";
|
||||
import { StatusIcon } from "../components/StatusIcon";
|
||||
@@ -56,30 +57,34 @@ export function Dashboard() {
|
||||
const { selectedCompanyId, selectedCompany } = useCompany();
|
||||
const { setBreadcrumbs } = useBreadcrumbs();
|
||||
const navigate = useNavigate();
|
||||
const { data: agents } = useAgents(selectedCompanyId);
|
||||
|
||||
const { data: agents } = useQuery({
|
||||
queryKey: queryKeys.agents.list(selectedCompanyId!),
|
||||
queryFn: () => agentsApi.list(selectedCompanyId!),
|
||||
enabled: !!selectedCompanyId,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setBreadcrumbs([{ label: "Dashboard" }]);
|
||||
}, [setBreadcrumbs]);
|
||||
|
||||
const dashFetcher = useCallback(() => {
|
||||
if (!selectedCompanyId) return Promise.resolve(null);
|
||||
return dashboardApi.summary(selectedCompanyId);
|
||||
}, [selectedCompanyId]);
|
||||
const { data, isLoading, error } = useQuery({
|
||||
queryKey: queryKeys.dashboard(selectedCompanyId!),
|
||||
queryFn: () => dashboardApi.summary(selectedCompanyId!),
|
||||
enabled: !!selectedCompanyId,
|
||||
});
|
||||
|
||||
const activityFetcher = useCallback(() => {
|
||||
if (!selectedCompanyId) return Promise.resolve([]);
|
||||
return activityApi.list(selectedCompanyId);
|
||||
}, [selectedCompanyId]);
|
||||
const { data: activity } = useQuery({
|
||||
queryKey: queryKeys.activity(selectedCompanyId!),
|
||||
queryFn: () => activityApi.list(selectedCompanyId!),
|
||||
enabled: !!selectedCompanyId,
|
||||
});
|
||||
|
||||
const issuesFetcher = useCallback(() => {
|
||||
if (!selectedCompanyId) return Promise.resolve([]);
|
||||
return issuesApi.list(selectedCompanyId);
|
||||
}, [selectedCompanyId]);
|
||||
|
||||
const { data, loading, error } = useApi(dashFetcher);
|
||||
const { data: activity } = useApi(activityFetcher);
|
||||
const { data: issues } = useApi(issuesFetcher);
|
||||
const { data: issues } = useQuery({
|
||||
queryKey: queryKeys.issues.list(selectedCompanyId!),
|
||||
queryFn: () => issuesApi.list(selectedCompanyId!),
|
||||
enabled: !!selectedCompanyId,
|
||||
});
|
||||
|
||||
const staleIssues = issues ? getStaleIssues(issues) : [];
|
||||
|
||||
@@ -103,7 +108,7 @@ export function Dashboard() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{loading && <p className="text-sm text-muted-foreground">Loading...</p>}
|
||||
{isLoading && <p className="text-sm text-muted-foreground">Loading...</p>}
|
||||
{error && <p className="text-sm text-destructive">{error.message}</p>}
|
||||
|
||||
{data && (
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { useEffect } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { goalsApi } from "../api/goals";
|
||||
import { projectsApi } from "../api/projects";
|
||||
import { useApi } from "../hooks/useApi";
|
||||
import { usePanel } from "../context/PanelContext";
|
||||
import { useCompany } from "../context/CompanyContext";
|
||||
import { useBreadcrumbs } from "../context/BreadcrumbContext";
|
||||
import { queryKeys } from "../lib/queryKeys";
|
||||
import { GoalProperties } from "../components/GoalProperties";
|
||||
import { GoalTree } from "../components/GoalTree";
|
||||
import { StatusBadge } from "../components/StatusBadge";
|
||||
@@ -20,24 +21,23 @@ export function GoalDetail() {
|
||||
const { setBreadcrumbs } = useBreadcrumbs();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const goalFetcher = useCallback(() => {
|
||||
if (!goalId) return Promise.reject(new Error("No goal ID"));
|
||||
return goalsApi.get(goalId);
|
||||
}, [goalId]);
|
||||
const { data: goal, isLoading, error } = useQuery({
|
||||
queryKey: queryKeys.goals.detail(goalId!),
|
||||
queryFn: () => goalsApi.get(goalId!),
|
||||
enabled: !!goalId,
|
||||
});
|
||||
|
||||
const allGoalsFetcher = useCallback(() => {
|
||||
if (!selectedCompanyId) return Promise.resolve([] as Goal[]);
|
||||
return goalsApi.list(selectedCompanyId);
|
||||
}, [selectedCompanyId]);
|
||||
const { data: allGoals } = useQuery({
|
||||
queryKey: queryKeys.goals.list(selectedCompanyId!),
|
||||
queryFn: () => goalsApi.list(selectedCompanyId!),
|
||||
enabled: !!selectedCompanyId,
|
||||
});
|
||||
|
||||
const projectsFetcher = useCallback(() => {
|
||||
if (!selectedCompanyId) return Promise.resolve([] as Project[]);
|
||||
return projectsApi.list(selectedCompanyId);
|
||||
}, [selectedCompanyId]);
|
||||
|
||||
const { data: goal, loading, error } = useApi(goalFetcher);
|
||||
const { data: allGoals } = useApi(allGoalsFetcher);
|
||||
const { data: allProjects } = useApi(projectsFetcher);
|
||||
const { data: allProjects } = useQuery({
|
||||
queryKey: queryKeys.projects.list(selectedCompanyId!),
|
||||
queryFn: () => projectsApi.list(selectedCompanyId!),
|
||||
enabled: !!selectedCompanyId,
|
||||
});
|
||||
|
||||
const childGoals = (allGoals ?? []).filter((g) => g.parentId === goalId);
|
||||
const linkedProjects = (allProjects ?? []).filter((p) => p.goalId === goalId);
|
||||
@@ -56,7 +56,7 @@ export function GoalDetail() {
|
||||
return () => closePanel();
|
||||
}, [goal]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
if (loading) return <p className="text-sm text-muted-foreground">Loading...</p>;
|
||||
if (isLoading) return <p className="text-sm text-muted-foreground">Loading...</p>;
|
||||
if (error) return <p className="text-sm text-destructive">{error.message}</p>;
|
||||
if (!goal) return null;
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { goalsApi } from "../api/goals";
|
||||
import { useApi } from "../hooks/useApi";
|
||||
import { useCompany } from "../context/CompanyContext";
|
||||
import { useBreadcrumbs } from "../context/BreadcrumbContext";
|
||||
import { queryKeys } from "../lib/queryKeys";
|
||||
import { GoalTree } from "../components/GoalTree";
|
||||
import { EmptyState } from "../components/EmptyState";
|
||||
import { Target } from "lucide-react";
|
||||
@@ -17,12 +18,11 @@ export function Goals() {
|
||||
setBreadcrumbs([{ label: "Goals" }]);
|
||||
}, [setBreadcrumbs]);
|
||||
|
||||
const fetcher = useCallback(() => {
|
||||
if (!selectedCompanyId) return Promise.resolve([]);
|
||||
return goalsApi.list(selectedCompanyId);
|
||||
}, [selectedCompanyId]);
|
||||
|
||||
const { data: goals, loading, error } = useApi(fetcher);
|
||||
const { data: goals, isLoading, error } = useQuery({
|
||||
queryKey: queryKeys.goals.list(selectedCompanyId!),
|
||||
queryFn: () => goalsApi.list(selectedCompanyId!),
|
||||
enabled: !!selectedCompanyId,
|
||||
});
|
||||
|
||||
if (!selectedCompanyId) {
|
||||
return <EmptyState icon={Target} message="Select a company to view goals." />;
|
||||
@@ -32,7 +32,7 @@ export function Goals() {
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-lg font-semibold">Goals</h2>
|
||||
|
||||
{loading && <p className="text-sm text-muted-foreground">Loading...</p>}
|
||||
{isLoading && <p className="text-sm text-muted-foreground">Loading...</p>}
|
||||
{error && <p className="text-sm text-destructive">{error.message}</p>}
|
||||
|
||||
{goals && goals.length === 0 && (
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { approvalsApi } from "../api/approvals";
|
||||
import { dashboardApi } from "../api/dashboard";
|
||||
import { issuesApi } from "../api/issues";
|
||||
import { agentsApi } from "../api/agents";
|
||||
import { useCompany } from "../context/CompanyContext";
|
||||
import { useBreadcrumbs } from "../context/BreadcrumbContext";
|
||||
import { useAgents } from "../hooks/useAgents";
|
||||
import { useApi } from "../hooks/useApi";
|
||||
import { StatusBadge } from "../components/StatusBadge";
|
||||
import { queryKeys } from "../lib/queryKeys";
|
||||
import { StatusIcon } from "../components/StatusIcon";
|
||||
import { PriorityIcon } from "../components/PriorityIcon";
|
||||
import { EmptyState } from "../components/EmptyState";
|
||||
@@ -43,31 +43,36 @@ export function Inbox() {
|
||||
const { selectedCompanyId } = useCompany();
|
||||
const { setBreadcrumbs } = useBreadcrumbs();
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
const [actionError, setActionError] = useState<string | null>(null);
|
||||
const { data: agents } = useAgents(selectedCompanyId);
|
||||
|
||||
const { data: agents } = useQuery({
|
||||
queryKey: queryKeys.agents.list(selectedCompanyId!),
|
||||
queryFn: () => agentsApi.list(selectedCompanyId!),
|
||||
enabled: !!selectedCompanyId,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setBreadcrumbs([{ label: "Inbox" }]);
|
||||
}, [setBreadcrumbs]);
|
||||
|
||||
const approvalsFetcher = useCallback(() => {
|
||||
if (!selectedCompanyId) return Promise.resolve([]);
|
||||
return approvalsApi.list(selectedCompanyId, "pending");
|
||||
}, [selectedCompanyId]);
|
||||
const { data: approvals, isLoading, error } = useQuery({
|
||||
queryKey: queryKeys.approvals.list(selectedCompanyId!, "pending"),
|
||||
queryFn: () => approvalsApi.list(selectedCompanyId!, "pending"),
|
||||
enabled: !!selectedCompanyId,
|
||||
});
|
||||
|
||||
const dashboardFetcher = useCallback(() => {
|
||||
if (!selectedCompanyId) return Promise.resolve(null);
|
||||
return dashboardApi.summary(selectedCompanyId);
|
||||
}, [selectedCompanyId]);
|
||||
const { data: dashboard } = useQuery({
|
||||
queryKey: queryKeys.dashboard(selectedCompanyId!),
|
||||
queryFn: () => dashboardApi.summary(selectedCompanyId!),
|
||||
enabled: !!selectedCompanyId,
|
||||
});
|
||||
|
||||
const issuesFetcher = useCallback(() => {
|
||||
if (!selectedCompanyId) return Promise.resolve([]);
|
||||
return issuesApi.list(selectedCompanyId);
|
||||
}, [selectedCompanyId]);
|
||||
|
||||
const { data: approvals, loading, error, reload } = useApi(approvalsFetcher);
|
||||
const { data: dashboard } = useApi(dashboardFetcher);
|
||||
const { data: issues } = useApi(issuesFetcher);
|
||||
const { data: issues } = useQuery({
|
||||
queryKey: queryKeys.issues.list(selectedCompanyId!),
|
||||
queryFn: () => issuesApi.list(selectedCompanyId!),
|
||||
enabled: !!selectedCompanyId,
|
||||
});
|
||||
|
||||
const staleIssues = issues ? getStaleIssues(issues) : [];
|
||||
|
||||
@@ -77,25 +82,25 @@ export function Inbox() {
|
||||
return agent?.name ?? null;
|
||||
};
|
||||
|
||||
async function approve(id: string) {
|
||||
setActionError(null);
|
||||
try {
|
||||
await approvalsApi.approve(id);
|
||||
reload();
|
||||
} catch (err) {
|
||||
const approveMutation = useMutation({
|
||||
mutationFn: (id: string) => approvalsApi.approve(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.approvals.list(selectedCompanyId!, "pending") });
|
||||
},
|
||||
onError: (err) => {
|
||||
setActionError(err instanceof Error ? err.message : "Failed to approve");
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
async function reject(id: string) {
|
||||
setActionError(null);
|
||||
try {
|
||||
await approvalsApi.reject(id);
|
||||
reload();
|
||||
} catch (err) {
|
||||
const rejectMutation = useMutation({
|
||||
mutationFn: (id: string) => approvalsApi.reject(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.approvals.list(selectedCompanyId!, "pending") });
|
||||
},
|
||||
onError: (err) => {
|
||||
setActionError(err instanceof Error ? err.message : "Failed to reject");
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if (!selectedCompanyId) {
|
||||
return <EmptyState icon={InboxIcon} message="Select a company to view inbox." />;
|
||||
@@ -113,11 +118,11 @@ export function Inbox() {
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-lg font-semibold">Inbox</h2>
|
||||
|
||||
{loading && <p className="text-sm text-muted-foreground">Loading...</p>}
|
||||
{isLoading && <p className="text-sm text-muted-foreground">Loading...</p>}
|
||||
{error && <p className="text-sm text-destructive">{error.message}</p>}
|
||||
{actionError && <p className="text-sm text-destructive">{actionError}</p>}
|
||||
|
||||
{!loading && !hasContent && (
|
||||
{!isLoading && !hasContent && (
|
||||
<EmptyState icon={InboxIcon} message="You're all caught up!" />
|
||||
)}
|
||||
|
||||
@@ -152,14 +157,14 @@ export function Inbox() {
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="border-green-700 text-green-500 hover:bg-green-900/20"
|
||||
onClick={() => approve(approval.id)}
|
||||
onClick={() => approveMutation.mutate(approval.id)}
|
||||
>
|
||||
Approve
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
onClick={() => reject(approval.id)}
|
||||
onClick={() => rejectMutation.mutate(approval.id)}
|
||||
>
|
||||
Reject
|
||||
</Button>
|
||||
|
||||
@@ -1,34 +1,53 @@
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { useEffect } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { issuesApi } from "../api/issues";
|
||||
import { useApi } from "../hooks/useApi";
|
||||
import { useCompany } from "../context/CompanyContext";
|
||||
import { usePanel } from "../context/PanelContext";
|
||||
import { useBreadcrumbs } from "../context/BreadcrumbContext";
|
||||
import { queryKeys } from "../lib/queryKeys";
|
||||
import { InlineEditor } from "../components/InlineEditor";
|
||||
import { CommentThread } from "../components/CommentThread";
|
||||
import { IssueProperties } from "../components/IssueProperties";
|
||||
import { StatusIcon } from "../components/StatusIcon";
|
||||
import { PriorityIcon } from "../components/PriorityIcon";
|
||||
import type { IssueComment } from "@paperclip/shared";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
|
||||
export function IssueDetail() {
|
||||
const { issueId } = useParams<{ issueId: string }>();
|
||||
const { selectedCompanyId } = useCompany();
|
||||
const { openPanel, closePanel } = usePanel();
|
||||
const { setBreadcrumbs } = useBreadcrumbs();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const issueFetcher = useCallback(() => {
|
||||
if (!issueId) return Promise.reject(new Error("No issue ID"));
|
||||
return issuesApi.get(issueId);
|
||||
}, [issueId]);
|
||||
const { data: issue, isLoading, error } = useQuery({
|
||||
queryKey: queryKeys.issues.detail(issueId!),
|
||||
queryFn: () => issuesApi.get(issueId!),
|
||||
enabled: !!issueId,
|
||||
});
|
||||
|
||||
const commentsFetcher = useCallback(() => {
|
||||
if (!issueId) return Promise.resolve([] as IssueComment[]);
|
||||
return issuesApi.listComments(issueId);
|
||||
}, [issueId]);
|
||||
const { data: comments } = useQuery({
|
||||
queryKey: queryKeys.issues.comments(issueId!),
|
||||
queryFn: () => issuesApi.listComments(issueId!),
|
||||
enabled: !!issueId,
|
||||
});
|
||||
|
||||
const { data: issue, loading, error, reload: reloadIssue } = useApi(issueFetcher);
|
||||
const { data: comments, reload: reloadComments } = useApi(commentsFetcher);
|
||||
const updateIssue = useMutation({
|
||||
mutationFn: (data: Record<string, unknown>) => issuesApi.update(issueId!, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.issues.detail(issueId!) });
|
||||
if (selectedCompanyId) {
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.issues.list(selectedCompanyId) });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const addComment = useMutation({
|
||||
mutationFn: (body: string) => issuesApi.addComment(issueId!, body),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.issues.comments(issueId!) });
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setBreadcrumbs([
|
||||
@@ -37,28 +56,16 @@ export function IssueDetail() {
|
||||
]);
|
||||
}, [setBreadcrumbs, issue, issueId]);
|
||||
|
||||
async function handleUpdate(data: Record<string, unknown>) {
|
||||
if (!issueId) return;
|
||||
await issuesApi.update(issueId, data);
|
||||
reloadIssue();
|
||||
}
|
||||
|
||||
async function handleAddComment(body: string) {
|
||||
if (!issueId) return;
|
||||
await issuesApi.addComment(issueId, body);
|
||||
reloadComments();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (issue) {
|
||||
openPanel(
|
||||
<IssueProperties issue={issue} onUpdate={handleUpdate} />
|
||||
<IssueProperties issue={issue} onUpdate={(data) => updateIssue.mutate(data)} />
|
||||
);
|
||||
}
|
||||
return () => closePanel();
|
||||
}, [issue]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
if (loading) return <p className="text-sm text-muted-foreground">Loading...</p>;
|
||||
if (isLoading) return <p className="text-sm text-muted-foreground">Loading...</p>;
|
||||
if (error) return <p className="text-sm text-destructive">{error.message}</p>;
|
||||
if (!issue) return null;
|
||||
|
||||
@@ -68,25 +75,25 @@ export function IssueDetail() {
|
||||
<div className="flex items-center gap-2">
|
||||
<StatusIcon
|
||||
status={issue.status}
|
||||
onChange={(status) => handleUpdate({ status })}
|
||||
onChange={(status) => updateIssue.mutate({ status })}
|
||||
/>
|
||||
<PriorityIcon
|
||||
priority={issue.priority}
|
||||
onChange={(priority) => handleUpdate({ priority })}
|
||||
onChange={(priority) => updateIssue.mutate({ priority })}
|
||||
/>
|
||||
<span className="text-xs font-mono text-muted-foreground">{issue.id.slice(0, 8)}</span>
|
||||
</div>
|
||||
|
||||
<InlineEditor
|
||||
value={issue.title}
|
||||
onSave={(title) => handleUpdate({ title })}
|
||||
onSave={(title) => updateIssue.mutate({ title })}
|
||||
as="h2"
|
||||
className="text-xl font-bold"
|
||||
/>
|
||||
|
||||
<InlineEditor
|
||||
value={issue.description ?? ""}
|
||||
onSave={(description) => handleUpdate({ description })}
|
||||
onSave={(description) => updateIssue.mutate({ description })}
|
||||
as="p"
|
||||
className="text-sm text-muted-foreground"
|
||||
placeholder="Add a description..."
|
||||
@@ -98,7 +105,9 @@ export function IssueDetail() {
|
||||
|
||||
<CommentThread
|
||||
comments={comments ?? []}
|
||||
onAdd={handleAddComment}
|
||||
onAdd={async (body) => {
|
||||
await addComment.mutateAsync(body);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { issuesApi } from "../api/issues";
|
||||
import { useApi } from "../hooks/useApi";
|
||||
import { useAgents } from "../hooks/useAgents";
|
||||
import { agentsApi } from "../api/agents";
|
||||
import { useCompany } from "../context/CompanyContext";
|
||||
import { useDialog } from "../context/DialogContext";
|
||||
import { useBreadcrumbs } from "../context/BreadcrumbContext";
|
||||
import { queryKeys } from "../lib/queryKeys";
|
||||
import { groupBy } from "../lib/groupBy";
|
||||
import { StatusIcon } from "../components/StatusIcon";
|
||||
import { PriorityIcon } from "../components/PriorityIcon";
|
||||
@@ -43,30 +44,38 @@ export function Issues() {
|
||||
const { openNewIssue } = useDialog();
|
||||
const { setBreadcrumbs } = useBreadcrumbs();
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
const [tab, setTab] = useState<TabFilter>("all");
|
||||
const { data: agents } = useAgents(selectedCompanyId);
|
||||
|
||||
const { data: agents } = useQuery({
|
||||
queryKey: queryKeys.agents.list(selectedCompanyId!),
|
||||
queryFn: () => agentsApi.list(selectedCompanyId!),
|
||||
enabled: !!selectedCompanyId,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setBreadcrumbs([{ label: "Issues" }]);
|
||||
}, [setBreadcrumbs]);
|
||||
|
||||
const fetcher = useCallback(() => {
|
||||
if (!selectedCompanyId) return Promise.resolve([]);
|
||||
return issuesApi.list(selectedCompanyId);
|
||||
}, [selectedCompanyId]);
|
||||
const { data: issues, isLoading, error } = useQuery({
|
||||
queryKey: queryKeys.issues.list(selectedCompanyId!),
|
||||
queryFn: () => issuesApi.list(selectedCompanyId!),
|
||||
enabled: !!selectedCompanyId,
|
||||
});
|
||||
|
||||
const { data: issues, loading, error, reload } = useApi(fetcher);
|
||||
const updateStatus = useMutation({
|
||||
mutationFn: ({ id, status }: { id: string; status: string }) =>
|
||||
issuesApi.update(id, { status }),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.issues.list(selectedCompanyId!) });
|
||||
},
|
||||
});
|
||||
|
||||
const agentName = (id: string | null) => {
|
||||
if (!id || !agents) return null;
|
||||
return agents.find((a) => a.id === id)?.name ?? null;
|
||||
};
|
||||
|
||||
async function handleStatusChange(issue: Issue, status: string) {
|
||||
await issuesApi.update(issue.id, { status });
|
||||
reload();
|
||||
}
|
||||
|
||||
if (!selectedCompanyId) {
|
||||
return <EmptyState icon={CircleDot} message="Select a company to view issues." />;
|
||||
}
|
||||
@@ -96,7 +105,7 @@ export function Issues() {
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
|
||||
{loading && <p className="text-sm text-muted-foreground">Loading...</p>}
|
||||
{isLoading && <p className="text-sm text-muted-foreground">Loading...</p>}
|
||||
{error && <p className="text-sm text-destructive">{error.message}</p>}
|
||||
|
||||
{issues && filtered.length === 0 && (
|
||||
@@ -137,7 +146,7 @@ export function Issues() {
|
||||
<PriorityIcon priority={issue.priority} />
|
||||
<StatusIcon
|
||||
status={issue.status}
|
||||
onChange={(s) => handleStatusChange(issue, s)}
|
||||
onChange={(s) => updateStatus.mutate({ id: issue.id, status: s })}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { issuesApi } from "../api/issues";
|
||||
import { useCompany } from "../context/CompanyContext";
|
||||
import { useBreadcrumbs } from "../context/BreadcrumbContext";
|
||||
import { useApi } from "../hooks/useApi";
|
||||
import { queryKeys } from "../lib/queryKeys";
|
||||
import { StatusIcon } from "../components/StatusIcon";
|
||||
import { PriorityIcon } from "../components/PriorityIcon";
|
||||
import { EntityRow } from "../components/EntityRow";
|
||||
@@ -20,12 +21,11 @@ export function MyIssues() {
|
||||
setBreadcrumbs([{ label: "My Issues" }]);
|
||||
}, [setBreadcrumbs]);
|
||||
|
||||
const fetcher = useCallback(() => {
|
||||
if (!selectedCompanyId) return Promise.resolve([]);
|
||||
return issuesApi.list(selectedCompanyId);
|
||||
}, [selectedCompanyId]);
|
||||
|
||||
const { data: issues, loading, error } = useApi(fetcher);
|
||||
const { data: issues, isLoading, error } = useQuery({
|
||||
queryKey: queryKeys.issues.list(selectedCompanyId!),
|
||||
queryFn: () => issuesApi.list(selectedCompanyId!),
|
||||
enabled: !!selectedCompanyId,
|
||||
});
|
||||
|
||||
if (!selectedCompanyId) {
|
||||
return <EmptyState icon={ListTodo} message="Select a company to view your issues." />;
|
||||
@@ -40,10 +40,10 @@ export function MyIssues() {
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-lg font-semibold">My Issues</h2>
|
||||
|
||||
{loading && <p className="text-sm text-muted-foreground">Loading...</p>}
|
||||
{isLoading && <p className="text-sm text-muted-foreground">Loading...</p>}
|
||||
{error && <p className="text-sm text-destructive">{error.message}</p>}
|
||||
|
||||
{!loading && myIssues.length === 0 && (
|
||||
{!isLoading && myIssues.length === 0 && (
|
||||
<EmptyState icon={ListTodo} message="No issues assigned to you." />
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { agentsApi, type OrgNode } from "../api/agents";
|
||||
import { useCompany } from "../context/CompanyContext";
|
||||
import { useBreadcrumbs } from "../context/BreadcrumbContext";
|
||||
import { useApi } from "../hooks/useApi";
|
||||
import { queryKeys } from "../lib/queryKeys";
|
||||
import { StatusBadge } from "../components/StatusBadge";
|
||||
import { EmptyState } from "../components/EmptyState";
|
||||
import { ChevronRight, GitBranch } from "lucide-react";
|
||||
@@ -93,12 +94,11 @@ export function Org() {
|
||||
setBreadcrumbs([{ label: "Org Chart" }]);
|
||||
}, [setBreadcrumbs]);
|
||||
|
||||
const fetcher = useCallback(() => {
|
||||
if (!selectedCompanyId) return Promise.resolve([] as OrgNode[]);
|
||||
return agentsApi.org(selectedCompanyId);
|
||||
}, [selectedCompanyId]);
|
||||
|
||||
const { data, loading, error } = useApi(fetcher);
|
||||
const { data, isLoading, error } = useQuery({
|
||||
queryKey: queryKeys.org(selectedCompanyId!),
|
||||
queryFn: () => agentsApi.org(selectedCompanyId!),
|
||||
enabled: !!selectedCompanyId,
|
||||
});
|
||||
|
||||
if (!selectedCompanyId) {
|
||||
return <EmptyState icon={GitBranch} message="Select a company to view org chart." />;
|
||||
@@ -108,7 +108,7 @@ export function Org() {
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-lg font-semibold">Org Chart</h2>
|
||||
|
||||
{loading && <p className="text-sm text-muted-foreground">Loading...</p>}
|
||||
{isLoading && <p className="text-sm text-muted-foreground">Loading...</p>}
|
||||
{error && <p className="text-sm text-destructive">{error.message}</p>}
|
||||
|
||||
{data && data.length === 0 && (
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { useEffect } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { projectsApi } from "../api/projects";
|
||||
import { issuesApi } from "../api/issues";
|
||||
import { useApi } from "../hooks/useApi";
|
||||
import { usePanel } from "../context/PanelContext";
|
||||
import { useCompany } from "../context/CompanyContext";
|
||||
import { useBreadcrumbs } from "../context/BreadcrumbContext";
|
||||
import { queryKeys } from "../lib/queryKeys";
|
||||
import { ProjectProperties } from "../components/ProjectProperties";
|
||||
import { StatusBadge } from "../components/StatusBadge";
|
||||
import { EntityRow } from "../components/EntityRow";
|
||||
@@ -18,18 +19,17 @@ export function ProjectDetail() {
|
||||
const { openPanel, closePanel } = usePanel();
|
||||
const { setBreadcrumbs } = useBreadcrumbs();
|
||||
|
||||
const projectFetcher = useCallback(() => {
|
||||
if (!projectId) return Promise.reject(new Error("No project ID"));
|
||||
return projectsApi.get(projectId);
|
||||
}, [projectId]);
|
||||
const { data: project, isLoading, error } = useQuery({
|
||||
queryKey: queryKeys.projects.detail(projectId!),
|
||||
queryFn: () => projectsApi.get(projectId!),
|
||||
enabled: !!projectId,
|
||||
});
|
||||
|
||||
const issuesFetcher = useCallback(() => {
|
||||
if (!selectedCompanyId) return Promise.resolve([] as Issue[]);
|
||||
return issuesApi.list(selectedCompanyId);
|
||||
}, [selectedCompanyId]);
|
||||
|
||||
const { data: project, loading, error } = useApi(projectFetcher);
|
||||
const { data: allIssues } = useApi(issuesFetcher);
|
||||
const { data: allIssues } = useQuery({
|
||||
queryKey: queryKeys.issues.list(selectedCompanyId!),
|
||||
queryFn: () => issuesApi.list(selectedCompanyId!),
|
||||
enabled: !!selectedCompanyId,
|
||||
});
|
||||
|
||||
const projectIssues = (allIssues ?? []).filter((i) => i.projectId === projectId);
|
||||
|
||||
@@ -47,7 +47,7 @@ export function ProjectDetail() {
|
||||
return () => closePanel();
|
||||
}, [project]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
if (loading) return <p className="text-sm text-muted-foreground">Loading...</p>;
|
||||
if (isLoading) return <p className="text-sm text-muted-foreground">Loading...</p>;
|
||||
if (error) return <p className="text-sm text-destructive">{error.message}</p>;
|
||||
if (!project) return null;
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { projectsApi } from "../api/projects";
|
||||
import { useApi } from "../hooks/useApi";
|
||||
import { useCompany } from "../context/CompanyContext";
|
||||
import { useDialog } from "../context/DialogContext";
|
||||
import { useBreadcrumbs } from "../context/BreadcrumbContext";
|
||||
import { queryKeys } from "../lib/queryKeys";
|
||||
import { EntityRow } from "../components/EntityRow";
|
||||
import { StatusBadge } from "../components/StatusBadge";
|
||||
import { EmptyState } from "../components/EmptyState";
|
||||
@@ -22,12 +23,11 @@ export function Projects() {
|
||||
setBreadcrumbs([{ label: "Projects" }]);
|
||||
}, [setBreadcrumbs]);
|
||||
|
||||
const fetcher = useCallback(() => {
|
||||
if (!selectedCompanyId) return Promise.resolve([]);
|
||||
return projectsApi.list(selectedCompanyId);
|
||||
}, [selectedCompanyId]);
|
||||
|
||||
const { data: projects, loading, error } = useApi(fetcher);
|
||||
const { data: projects, isLoading, error } = useQuery({
|
||||
queryKey: queryKeys.projects.list(selectedCompanyId!),
|
||||
queryFn: () => projectsApi.list(selectedCompanyId!),
|
||||
enabled: !!selectedCompanyId,
|
||||
});
|
||||
|
||||
if (!selectedCompanyId) {
|
||||
return <EmptyState icon={Hexagon} message="Select a company to view projects." />;
|
||||
@@ -43,7 +43,7 @@ export function Projects() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{loading && <p className="text-sm text-muted-foreground">Loading...</p>}
|
||||
{isLoading && <p className="text-sm text-muted-foreground">Loading...</p>}
|
||||
{error && <p className="text-sm text-destructive">{error.message}</p>}
|
||||
|
||||
{projects && projects.length === 0 && (
|
||||
|
||||
Reference in New Issue
Block a user