feat: add is_chat field to board memory and task_id to approvals, update pagination and response models
This commit is contained in:
@@ -116,7 +116,7 @@ export default function EditAgentPage() {
|
||||
const boardsQuery = useListBoardsApiV1BoardsGet<
|
||||
listBoardsApiV1BoardsGetResponse,
|
||||
ApiError
|
||||
>({
|
||||
>(undefined, {
|
||||
query: {
|
||||
enabled: Boolean(isSignedIn),
|
||||
refetchOnMount: "always",
|
||||
@@ -148,7 +148,8 @@ export default function EditAgentPage() {
|
||||
},
|
||||
});
|
||||
|
||||
const boards = boardsQuery.data?.status === 200 ? boardsQuery.data.data : [];
|
||||
const boards =
|
||||
boardsQuery.data?.status === 200 ? boardsQuery.data.data.items ?? [] : [];
|
||||
const loadedAgent: AgentRead | null =
|
||||
agentQuery.data?.status === 200 ? agentQuery.data.data : null;
|
||||
|
||||
|
||||
@@ -106,7 +106,7 @@ export default function AgentDetailPage() {
|
||||
const boardsQuery = useListBoardsApiV1BoardsGet<
|
||||
listBoardsApiV1BoardsGetResponse,
|
||||
ApiError
|
||||
>({
|
||||
>(undefined, {
|
||||
query: {
|
||||
enabled: Boolean(isSignedIn),
|
||||
refetchInterval: 60_000,
|
||||
@@ -118,9 +118,11 @@ export default function AgentDetailPage() {
|
||||
const agent: AgentRead | null =
|
||||
agentQuery.data?.status === 200 ? agentQuery.data.data : null;
|
||||
const events: ActivityEventRead[] =
|
||||
activityQuery.data?.status === 200 ? activityQuery.data.data : [];
|
||||
activityQuery.data?.status === 200
|
||||
? activityQuery.data.data.items ?? []
|
||||
: [];
|
||||
const boards: BoardRead[] =
|
||||
boardsQuery.data?.status === 200 ? boardsQuery.data.data : [];
|
||||
boardsQuery.data?.status === 200 ? boardsQuery.data.data.items ?? [] : [];
|
||||
|
||||
const agentEvents = useMemo(() => {
|
||||
if (!agent) return [];
|
||||
|
||||
@@ -91,7 +91,7 @@ export default function NewAgentPage() {
|
||||
const boardsQuery = useListBoardsApiV1BoardsGet<
|
||||
listBoardsApiV1BoardsGetResponse,
|
||||
ApiError
|
||||
>({
|
||||
>(undefined, {
|
||||
query: {
|
||||
enabled: Boolean(isSignedIn),
|
||||
refetchOnMount: "always",
|
||||
@@ -111,7 +111,8 @@ export default function NewAgentPage() {
|
||||
},
|
||||
});
|
||||
|
||||
const boards = boardsQuery.data?.status === 200 ? boardsQuery.data.data : [];
|
||||
const boards =
|
||||
boardsQuery.data?.status === 200 ? boardsQuery.data.data.items ?? [] : [];
|
||||
const displayBoardId = boardId || boards[0]?.id || "";
|
||||
const isLoading = boardsQuery.isLoading || createAgentMutation.isPending;
|
||||
const errorMessage = error ?? boardsQuery.error?.message ?? null;
|
||||
|
||||
@@ -98,7 +98,7 @@ export default function AgentsPage() {
|
||||
const boardsQuery = useListBoardsApiV1BoardsGet<
|
||||
listBoardsApiV1BoardsGetResponse,
|
||||
ApiError
|
||||
>({
|
||||
>(undefined, {
|
||||
query: {
|
||||
enabled: Boolean(isSignedIn),
|
||||
refetchInterval: 30_000,
|
||||
@@ -109,7 +109,7 @@ export default function AgentsPage() {
|
||||
const agentsQuery = useListAgentsApiV1AgentsGet<
|
||||
listAgentsApiV1AgentsGetResponse,
|
||||
ApiError
|
||||
>({
|
||||
>(undefined, {
|
||||
query: {
|
||||
enabled: Boolean(isSignedIn),
|
||||
refetchInterval: 15_000,
|
||||
@@ -118,10 +118,15 @@ export default function AgentsPage() {
|
||||
});
|
||||
|
||||
const boards = useMemo(
|
||||
() => (boardsQuery.data?.status === 200 ? boardsQuery.data.data : []),
|
||||
() =>
|
||||
boardsQuery.data?.status === 200 ? boardsQuery.data.data.items ?? [] : [],
|
||||
[boardsQuery.data]
|
||||
);
|
||||
const agents = useMemo(() => agentsQuery.data?.data ?? [], [agentsQuery.data]);
|
||||
const agents = useMemo(
|
||||
() =>
|
||||
agentsQuery.data?.status === 200 ? agentsQuery.data.data.items ?? [] : [],
|
||||
[agentsQuery.data]
|
||||
);
|
||||
|
||||
const deleteMutation = useDeleteAgentApiV1AgentsAgentIdDelete<
|
||||
ApiError,
|
||||
@@ -133,10 +138,18 @@ export default function AgentsPage() {
|
||||
await queryClient.cancelQueries({ queryKey: agentsKey });
|
||||
const previous =
|
||||
queryClient.getQueryData<listAgentsApiV1AgentsGetResponse>(agentsKey);
|
||||
if (previous) {
|
||||
if (previous && previous.status === 200) {
|
||||
const nextItems = previous.data.items.filter(
|
||||
(agent) => agent.id !== agentId
|
||||
);
|
||||
const removedCount = previous.data.items.length - nextItems.length;
|
||||
queryClient.setQueryData<listAgentsApiV1AgentsGetResponse>(agentsKey, {
|
||||
...previous,
|
||||
data: previous.data.filter((agent) => agent.id !== agentId),
|
||||
data: {
|
||||
...previous.data,
|
||||
items: nextItems,
|
||||
total: Math.max(0, previous.data.total - removedCount),
|
||||
},
|
||||
});
|
||||
}
|
||||
return { previous };
|
||||
|
||||
@@ -16,8 +16,6 @@ import {
|
||||
useListGatewaysApiV1GatewaysGet,
|
||||
} from "@/api/generated/gateways/gateways";
|
||||
import type { BoardRead, BoardUpdate } from "@/api/generated/model";
|
||||
import { BoardApprovalsPanel } from "@/components/BoardApprovalsPanel";
|
||||
import { BoardGoalPanel } from "@/components/BoardGoalPanel";
|
||||
import { BoardOnboardingChat } from "@/components/BoardOnboardingChat";
|
||||
import { DashboardSidebar } from "@/components/organisms/DashboardSidebar";
|
||||
import { DashboardShell } from "@/components/templates/DashboardShell";
|
||||
@@ -72,7 +70,7 @@ export default function EditBoardPage() {
|
||||
const gatewaysQuery = useListGatewaysApiV1GatewaysGet<
|
||||
listGatewaysApiV1GatewaysGetResponse,
|
||||
ApiError
|
||||
>({
|
||||
>(undefined, {
|
||||
query: {
|
||||
enabled: Boolean(isSignedIn),
|
||||
refetchOnMount: "always",
|
||||
@@ -105,7 +103,9 @@ export default function EditBoardPage() {
|
||||
});
|
||||
|
||||
const gateways =
|
||||
gatewaysQuery.data?.status === 200 ? gatewaysQuery.data.data : [];
|
||||
gatewaysQuery.data?.status === 200
|
||||
? gatewaysQuery.data.data.items ?? []
|
||||
: [];
|
||||
const loadedBoard: BoardRead | null =
|
||||
boardQuery.data?.status === 200 ? boardQuery.data.data : null;
|
||||
const baseBoard = board ?? loadedBoard;
|
||||
@@ -224,135 +224,149 @@ export default function EditBoardPage() {
|
||||
</div>
|
||||
|
||||
<div className="p-8">
|
||||
<div className="grid gap-6 xl:grid-cols-[minmax(0,1fr)_360px]">
|
||||
<div className="space-y-6">
|
||||
<BoardGoalPanel
|
||||
board={baseBoard}
|
||||
onStartOnboarding={() => setIsOnboardingOpen(true)}
|
||||
/>
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="space-y-6 rounded-xl border border-slate-200 bg-white p-6 shadow-sm"
|
||||
>
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-slate-900">
|
||||
Board name <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<Input
|
||||
value={resolvedName}
|
||||
onChange={(event) => setName(event.target.value)}
|
||||
placeholder="Board name"
|
||||
disabled={isLoading || !baseBoard}
|
||||
/>
|
||||
<div className="space-y-6">
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="space-y-6 rounded-xl border border-slate-200 bg-white p-6 shadow-sm"
|
||||
>
|
||||
{resolvedBoardType !== "general" &&
|
||||
baseBoard &&
|
||||
!(baseBoard.goal_confirmed ?? false) ? (
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 rounded-xl border border-amber-200 bg-amber-50 px-4 py-3">
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-semibold text-amber-900">
|
||||
Goal needs confirmation
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-amber-800/80">
|
||||
Start onboarding to draft an objective and success
|
||||
metrics.
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-slate-900">
|
||||
Gateway <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<SearchableSelect
|
||||
ariaLabel="Select gateway"
|
||||
value={displayGatewayId}
|
||||
onValueChange={setGatewayId}
|
||||
options={gatewayOptions}
|
||||
placeholder="Select gateway"
|
||||
searchPlaceholder="Search gateways..."
|
||||
emptyMessage="No gateways found."
|
||||
triggerClassName="w-full h-11 rounded-xl border border-slate-300 bg-white px-3 py-2 text-sm font-medium text-slate-900 shadow-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-200"
|
||||
contentClassName="rounded-xl border border-slate-200 shadow-lg"
|
||||
itemClassName="px-4 py-3 text-sm text-slate-700 data-[selected=true]:bg-slate-50 data-[selected=true]:text-slate-900"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-slate-900">
|
||||
Board type
|
||||
</label>
|
||||
<Select value={resolvedBoardType} onValueChange={setBoardType}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select board type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="goal">Goal</SelectItem>
|
||||
<SelectItem value="general">General</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-slate-900">
|
||||
Target date
|
||||
</label>
|
||||
<Input
|
||||
type="date"
|
||||
value={resolvedTargetDate}
|
||||
onChange={(event) => setTargetDate(event.target.value)}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-slate-900">
|
||||
Objective
|
||||
</label>
|
||||
<Textarea
|
||||
value={resolvedObjective}
|
||||
onChange={(event) => setObjective(event.target.value)}
|
||||
placeholder="What should this board achieve?"
|
||||
className="min-h-[120px]"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-slate-900">
|
||||
Success metrics (JSON)
|
||||
</label>
|
||||
<Textarea
|
||||
value={resolvedSuccessMetrics}
|
||||
onChange={(event) => setSuccessMetrics(event.target.value)}
|
||||
placeholder='e.g. { "target": "Launch by week 2" }'
|
||||
className="min-h-[140px] font-mono text-xs"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<p className="text-xs text-slate-500">
|
||||
Add key outcomes so the lead agent can measure progress.
|
||||
</p>
|
||||
{metricsError ? (
|
||||
<p className="text-xs text-red-500">{metricsError}</p>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{gateways.length === 0 ? (
|
||||
<div className="rounded-lg border border-slate-200 bg-slate-50 px-4 py-3 text-sm text-slate-600">
|
||||
<p>No gateways available. Create one in Gateways to continue.</p>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{errorMessage ? (
|
||||
<p className="text-sm text-red-500">{errorMessage}</p>
|
||||
) : null}
|
||||
|
||||
<div className="flex justify-end gap-3">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onClick={() => router.push(`/boards/${boardId}`)}
|
||||
disabled={isLoading}
|
||||
variant="secondary"
|
||||
onClick={() => setIsOnboardingOpen(true)}
|
||||
disabled={isLoading || !baseBoard}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={isLoading || !baseBoard || !isFormReady}>
|
||||
{isLoading ? "Saving…" : "Save changes"}
|
||||
Start onboarding
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div className="space-y-6">
|
||||
{boardId ? <BoardApprovalsPanel boardId={boardId} /> : null}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-slate-900">
|
||||
Board name <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<Input
|
||||
value={resolvedName}
|
||||
onChange={(event) => setName(event.target.value)}
|
||||
placeholder="Board name"
|
||||
disabled={isLoading || !baseBoard}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-slate-900">
|
||||
Gateway <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<SearchableSelect
|
||||
ariaLabel="Select gateway"
|
||||
value={displayGatewayId}
|
||||
onValueChange={setGatewayId}
|
||||
options={gatewayOptions}
|
||||
placeholder="Select gateway"
|
||||
searchPlaceholder="Search gateways..."
|
||||
emptyMessage="No gateways found."
|
||||
triggerClassName="w-full h-11 rounded-xl border border-slate-300 bg-white px-3 py-2 text-sm font-medium text-slate-900 shadow-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-200"
|
||||
contentClassName="rounded-xl border border-slate-200 shadow-lg"
|
||||
itemClassName="px-4 py-3 text-sm text-slate-700 data-[selected=true]:bg-slate-50 data-[selected=true]:text-slate-900"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-slate-900">
|
||||
Board type
|
||||
</label>
|
||||
<Select value={resolvedBoardType} onValueChange={setBoardType}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select board type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="goal">Goal</SelectItem>
|
||||
<SelectItem value="general">General</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-slate-900">
|
||||
Target date
|
||||
</label>
|
||||
<Input
|
||||
type="date"
|
||||
value={resolvedTargetDate}
|
||||
onChange={(event) => setTargetDate(event.target.value)}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-slate-900">
|
||||
Objective
|
||||
</label>
|
||||
<Textarea
|
||||
value={resolvedObjective}
|
||||
onChange={(event) => setObjective(event.target.value)}
|
||||
placeholder="What should this board achieve?"
|
||||
className="min-h-[120px]"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-slate-900">
|
||||
Success metrics (JSON)
|
||||
</label>
|
||||
<Textarea
|
||||
value={resolvedSuccessMetrics}
|
||||
onChange={(event) => setSuccessMetrics(event.target.value)}
|
||||
placeholder='e.g. { "target": "Launch by week 2" }'
|
||||
className="min-h-[140px] font-mono text-xs"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<p className="text-xs text-slate-500">
|
||||
Add key outcomes so the lead agent can measure progress.
|
||||
</p>
|
||||
{metricsError ? (
|
||||
<p className="text-xs text-red-500">{metricsError}</p>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{gateways.length === 0 ? (
|
||||
<div className="rounded-lg border border-slate-200 bg-slate-50 px-4 py-3 text-sm text-slate-600">
|
||||
<p>No gateways available. Create one in Gateways to continue.</p>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{errorMessage ? (
|
||||
<p className="text-sm text-red-500">{errorMessage}</p>
|
||||
) : null}
|
||||
|
||||
<div className="flex justify-end gap-3">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onClick={() => router.push(`/boards/${boardId}`)}
|
||||
disabled={isLoading}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={isLoading || !baseBoard || !isFormReady}>
|
||||
{isLoading ? "Saving…" : "Save changes"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -28,16 +28,14 @@ import {
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { listAgentsApiV1AgentsGet, streamAgentsApiV1AgentsStreamGet } from "@/api/generated/agents/agents";
|
||||
import { streamAgentsApiV1AgentsStreamGet } from "@/api/generated/agents/agents";
|
||||
import {
|
||||
listApprovalsApiV1BoardsBoardIdApprovalsGet,
|
||||
streamApprovalsApiV1BoardsBoardIdApprovalsStreamGet,
|
||||
updateApprovalApiV1BoardsBoardIdApprovalsApprovalIdPatch,
|
||||
} from "@/api/generated/approvals/approvals";
|
||||
import { getBoardApiV1BoardsBoardIdGet } from "@/api/generated/boards/boards";
|
||||
import { getBoardSnapshotApiV1BoardsBoardIdSnapshotGet } from "@/api/generated/boards/boards";
|
||||
import {
|
||||
createBoardMemoryApiV1BoardsBoardIdMemoryPost,
|
||||
listBoardMemoryApiV1BoardsBoardIdMemoryGet,
|
||||
streamBoardMemoryApiV1BoardsBoardIdMemoryStreamGet,
|
||||
} from "@/api/generated/board-memory/board-memory";
|
||||
import {
|
||||
@@ -45,7 +43,6 @@ import {
|
||||
createTaskCommentApiV1BoardsBoardIdTasksTaskIdCommentsPost,
|
||||
deleteTaskApiV1BoardsBoardIdTasksTaskIdDelete,
|
||||
listTaskCommentsApiV1BoardsBoardIdTasksTaskIdCommentsGet,
|
||||
listTasksApiV1BoardsBoardIdTasksGet,
|
||||
streamTasksApiV1BoardsBoardIdTasksStreamGet,
|
||||
updateTaskApiV1BoardsBoardIdTasksTaskIdPatch,
|
||||
} from "@/api/generated/tasks/tasks";
|
||||
@@ -54,6 +51,7 @@ import type {
|
||||
ApprovalRead,
|
||||
BoardMemoryRead,
|
||||
BoardRead,
|
||||
TaskCardRead,
|
||||
TaskCommentRead,
|
||||
TaskRead,
|
||||
} from "@/api/generated/model";
|
||||
@@ -61,13 +59,16 @@ import { cn } from "@/lib/utils";
|
||||
|
||||
type Board = BoardRead;
|
||||
|
||||
type TaskStatus = Exclude<TaskRead["status"], undefined>;
|
||||
type TaskStatus = Exclude<TaskCardRead["status"], undefined>;
|
||||
|
||||
type Task = TaskRead & {
|
||||
type Task = Omit<
|
||||
TaskCardRead,
|
||||
"status" | "priority" | "approvals_count" | "approvals_pending_count"
|
||||
> & {
|
||||
status: TaskStatus;
|
||||
priority: string;
|
||||
approvalsCount?: number;
|
||||
approvalsPendingCount?: number;
|
||||
approvals_count: number;
|
||||
approvals_pending_count: number;
|
||||
};
|
||||
|
||||
type Agent = AgentRead & { status: string };
|
||||
@@ -78,10 +79,12 @@ type Approval = ApprovalRead & { status: string };
|
||||
|
||||
type BoardChatMessage = BoardMemoryRead;
|
||||
|
||||
const normalizeTask = (task: TaskRead): Task => ({
|
||||
const normalizeTask = (task: TaskCardRead): Task => ({
|
||||
...task,
|
||||
status: task.status ?? "inbox",
|
||||
priority: task.priority ?? "medium",
|
||||
approvals_count: task.approvals_count ?? 0,
|
||||
approvals_pending_count: task.approvals_pending_count ?? 0,
|
||||
});
|
||||
|
||||
const normalizeAgent = (agent: AgentRead): Agent => ({
|
||||
@@ -94,15 +97,6 @@ const normalizeApproval = (approval: ApprovalRead): Approval => ({
|
||||
status: approval.status ?? "pending",
|
||||
});
|
||||
|
||||
const approvalTaskId = (approval: Approval) => {
|
||||
const payload = approval.payload ?? {};
|
||||
return (
|
||||
(payload as Record<string, unknown>).task_id ??
|
||||
(payload as Record<string, unknown>).taskId ??
|
||||
(payload as Record<string, unknown>).taskID
|
||||
);
|
||||
};
|
||||
|
||||
const priorities = [
|
||||
{ value: "low", label: "Low" },
|
||||
{ value: "medium", label: "Medium" },
|
||||
@@ -244,31 +238,38 @@ export default function BoardDetailPage() {
|
||||
const loadBoard = async () => {
|
||||
if (!isSignedIn || !boardId) return;
|
||||
setIsLoading(true);
|
||||
setIsApprovalsLoading(true);
|
||||
setError(null);
|
||||
setApprovalsError(null);
|
||||
setChatError(null);
|
||||
try {
|
||||
const [boardResult, tasksResult, agentsResult] = await Promise.all([
|
||||
getBoardApiV1BoardsBoardIdGet(boardId),
|
||||
listTasksApiV1BoardsBoardIdTasksGet(boardId),
|
||||
listAgentsApiV1AgentsGet(),
|
||||
]);
|
||||
|
||||
if (boardResult.status !== 200) throw new Error("Unable to load board.");
|
||||
if (tasksResult.status !== 200) throw new Error("Unable to load tasks.");
|
||||
|
||||
setBoard(boardResult.data);
|
||||
setTasks(tasksResult.data.map(normalizeTask));
|
||||
setAgents(agentsResult.data.map(normalizeAgent));
|
||||
const snapshotResult = await getBoardSnapshotApiV1BoardsBoardIdSnapshotGet(
|
||||
boardId,
|
||||
);
|
||||
if (snapshotResult.status !== 200) {
|
||||
throw new Error("Unable to load board snapshot.");
|
||||
}
|
||||
const snapshot = snapshotResult.data;
|
||||
setBoard(snapshot.board);
|
||||
setTasks((snapshot.tasks ?? []).map(normalizeTask));
|
||||
setAgents((snapshot.agents ?? []).map(normalizeAgent));
|
||||
setApprovals((snapshot.approvals ?? []).map(normalizeApproval));
|
||||
setChatMessages(snapshot.chat_messages ?? []);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Something went wrong.");
|
||||
const message = err instanceof Error ? err.message : "Something went wrong.";
|
||||
setError(message);
|
||||
setApprovalsError(message);
|
||||
setChatError(message);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setIsApprovalsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadBoard();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [boardId, isSignedIn]);
|
||||
}, [board, boardId, isSignedIn]);
|
||||
|
||||
useEffect(() => {
|
||||
tasksRef.current = tasks;
|
||||
@@ -294,54 +295,6 @@ export default function BoardDetailPage() {
|
||||
return () => window.clearTimeout(timeout);
|
||||
}, [chatMessages, isChatOpen]);
|
||||
|
||||
const loadApprovals = useCallback(async () => {
|
||||
if (!isSignedIn || !boardId) return;
|
||||
setIsApprovalsLoading(true);
|
||||
setApprovalsError(null);
|
||||
try {
|
||||
const result = await listApprovalsApiV1BoardsBoardIdApprovalsGet(boardId);
|
||||
if (result.status !== 200) throw new Error("Unable to load approvals.");
|
||||
setApprovals(result.data.map(normalizeApproval));
|
||||
} catch (err) {
|
||||
setApprovalsError(
|
||||
err instanceof Error ? err.message : "Unable to load approvals.",
|
||||
);
|
||||
} finally {
|
||||
setIsApprovalsLoading(false);
|
||||
}
|
||||
}, [boardId, isSignedIn]);
|
||||
|
||||
useEffect(() => {
|
||||
loadApprovals();
|
||||
}, [boardId, isSignedIn, loadApprovals]);
|
||||
|
||||
const loadBoardChat = useCallback(async () => {
|
||||
if (!isSignedIn || !boardId) return;
|
||||
setChatError(null);
|
||||
try {
|
||||
const result = await listBoardMemoryApiV1BoardsBoardIdMemoryGet(boardId, {
|
||||
limit: 200,
|
||||
});
|
||||
if (result.status !== 200) throw new Error("Unable to load board chat.");
|
||||
const data = result.data;
|
||||
const chatOnly = data.filter((item) => item.tags?.includes("chat"));
|
||||
const ordered = chatOnly.sort((a, b) => {
|
||||
const aTime = new Date(a.created_at).getTime();
|
||||
const bTime = new Date(b.created_at).getTime();
|
||||
return aTime - bTime;
|
||||
});
|
||||
setChatMessages(ordered);
|
||||
} catch (err) {
|
||||
setChatError(
|
||||
err instanceof Error ? err.message : "Unable to load board chat.",
|
||||
);
|
||||
}
|
||||
}, [boardId, isSignedIn]);
|
||||
|
||||
useEffect(() => {
|
||||
loadBoardChat();
|
||||
}, [boardId, isSignedIn, loadBoardChat]);
|
||||
|
||||
const latestChatTimestamp = (items: BoardChatMessage[]) => {
|
||||
if (!items.length) return undefined;
|
||||
const latest = items.reduce((max, item) => {
|
||||
@@ -353,17 +306,18 @@ export default function BoardDetailPage() {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSignedIn || !boardId) return;
|
||||
if (!isSignedIn || !boardId || !board) return;
|
||||
let isCancelled = false;
|
||||
const abortController = new AbortController();
|
||||
|
||||
const connect = async () => {
|
||||
try {
|
||||
const since = latestChatTimestamp(chatMessagesRef.current);
|
||||
const params = { is_chat: true, ...(since ? { since } : {}) };
|
||||
const streamResult =
|
||||
await streamBoardMemoryApiV1BoardsBoardIdMemoryStreamGet(
|
||||
boardId,
|
||||
since ? { since } : undefined,
|
||||
params,
|
||||
{
|
||||
headers: { Accept: "text/event-stream" },
|
||||
signal: abortController.signal,
|
||||
@@ -439,7 +393,7 @@ export default function BoardDetailPage() {
|
||||
}, [boardId, isSignedIn]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSignedIn || !boardId) return;
|
||||
if (!isSignedIn || !boardId || !board) return;
|
||||
let isCancelled = false;
|
||||
const abortController = new AbortController();
|
||||
|
||||
@@ -487,7 +441,15 @@ export default function BoardDetailPage() {
|
||||
}
|
||||
if (eventType === "approval" && data) {
|
||||
try {
|
||||
const payload = JSON.parse(data) as { approval?: ApprovalRead };
|
||||
const payload = JSON.parse(data) as {
|
||||
approval?: ApprovalRead;
|
||||
task_counts?: {
|
||||
task_id?: string;
|
||||
approvals_count?: number;
|
||||
approvals_pending_count?: number;
|
||||
};
|
||||
pending_approvals_count?: number;
|
||||
};
|
||||
if (payload.approval) {
|
||||
const normalized = normalizeApproval(payload.approval);
|
||||
setApprovals((prev) => {
|
||||
@@ -505,6 +467,25 @@ export default function BoardDetailPage() {
|
||||
return next;
|
||||
});
|
||||
}
|
||||
if (payload.task_counts?.task_id) {
|
||||
const taskId = payload.task_counts.task_id;
|
||||
setTasks((prev) => {
|
||||
const index = prev.findIndex((task) => task.id === taskId);
|
||||
if (index === -1) return prev;
|
||||
const next = [...prev];
|
||||
const current = next[index];
|
||||
next[index] = {
|
||||
...current,
|
||||
approvals_count:
|
||||
payload.task_counts?.approvals_count ??
|
||||
current.approvals_count,
|
||||
approvals_pending_count:
|
||||
payload.task_counts?.approvals_pending_count ??
|
||||
current.approvals_pending_count,
|
||||
};
|
||||
return next;
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// Ignore malformed payloads.
|
||||
}
|
||||
@@ -524,7 +505,7 @@ export default function BoardDetailPage() {
|
||||
isCancelled = true;
|
||||
abortController.abort();
|
||||
};
|
||||
}, [boardId, isSignedIn]);
|
||||
}, [board, boardId, isSignedIn]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedTask) {
|
||||
@@ -610,14 +591,37 @@ export default function BoardDetailPage() {
|
||||
return [...prev, payload.comment as TaskComment];
|
||||
});
|
||||
} else if (payload.task) {
|
||||
const normalizedTask = normalizeTask(payload.task);
|
||||
setTasks((prev) => {
|
||||
const index = prev.findIndex((item) => item.id === normalizedTask.id);
|
||||
const index = prev.findIndex((item) => item.id === payload.task?.id);
|
||||
if (index === -1) {
|
||||
return [normalizedTask, ...prev];
|
||||
const assignee = payload.task?.assigned_agent_id
|
||||
? agentsRef.current.find(
|
||||
(agent) => agent.id === payload.task?.assigned_agent_id,
|
||||
)?.name ?? null
|
||||
: null;
|
||||
const created = normalizeTask({
|
||||
...payload.task,
|
||||
assignee,
|
||||
approvals_count: 0,
|
||||
approvals_pending_count: 0,
|
||||
} as TaskCardRead);
|
||||
return [created, ...prev];
|
||||
}
|
||||
const next = [...prev];
|
||||
next[index] = { ...next[index], ...normalizedTask };
|
||||
const existing = next[index];
|
||||
const assignee = payload.task?.assigned_agent_id
|
||||
? agentsRef.current.find(
|
||||
(agent) => agent.id === payload.task?.assigned_agent_id,
|
||||
)?.name ?? null
|
||||
: null;
|
||||
const updated = normalizeTask({
|
||||
...existing,
|
||||
...payload.task,
|
||||
assignee,
|
||||
approvals_count: existing.approvals_count,
|
||||
approvals_pending_count: existing.approvals_pending_count,
|
||||
} as TaskCardRead);
|
||||
next[index] = { ...existing, ...updated };
|
||||
return next;
|
||||
});
|
||||
}
|
||||
@@ -727,7 +731,7 @@ export default function BoardDetailPage() {
|
||||
isCancelled = true;
|
||||
abortController.abort();
|
||||
};
|
||||
}, [boardId, isSignedIn]);
|
||||
}, [board, boardId, isSignedIn]);
|
||||
|
||||
const resetForm = () => {
|
||||
setTitle("");
|
||||
@@ -754,7 +758,14 @@ export default function BoardDetailPage() {
|
||||
});
|
||||
if (result.status !== 200) throw new Error("Unable to create task.");
|
||||
|
||||
const created = normalizeTask(result.data);
|
||||
const created = normalizeTask({
|
||||
...result.data,
|
||||
assignee: result.data.assigned_agent_id
|
||||
? assigneeById.get(result.data.assigned_agent_id) ?? null
|
||||
: null,
|
||||
approvals_count: 0,
|
||||
approvals_pending_count: 0,
|
||||
} as TaskCardRead);
|
||||
setTasks((prev) => [created, ...prev]);
|
||||
setIsDialogOpen(false);
|
||||
resetForm();
|
||||
@@ -829,49 +840,9 @@ export default function BoardDetailPage() {
|
||||
});
|
||||
}, [liveFeed]);
|
||||
|
||||
const pendingApprovalsByTaskId = useMemo(() => {
|
||||
const map = new Map<string, number>();
|
||||
approvals
|
||||
.filter((approval) => approval.status === "pending")
|
||||
.forEach((approval) => {
|
||||
const taskId = approvalTaskId(approval);
|
||||
if (!taskId || typeof taskId !== "string") return;
|
||||
map.set(taskId, (map.get(taskId) ?? 0) + 1);
|
||||
});
|
||||
return map;
|
||||
}, [approvals]);
|
||||
|
||||
const totalApprovalsByTaskId = useMemo(() => {
|
||||
const map = new Map<string, number>();
|
||||
approvals.forEach((approval) => {
|
||||
const taskId = approvalTaskId(approval);
|
||||
if (!taskId || typeof taskId !== "string") return;
|
||||
map.set(taskId, (map.get(taskId) ?? 0) + 1);
|
||||
});
|
||||
return map;
|
||||
}, [approvals]);
|
||||
|
||||
const displayTasks = useMemo(
|
||||
() =>
|
||||
tasks.map((task) => ({
|
||||
...task,
|
||||
assignee: task.assigned_agent_id
|
||||
? assigneeById.get(task.assigned_agent_id)
|
||||
: undefined,
|
||||
approvalsCount: totalApprovalsByTaskId.get(task.id) ?? 0,
|
||||
approvalsPendingCount: pendingApprovalsByTaskId.get(task.id) ?? 0,
|
||||
})),
|
||||
[tasks, assigneeById, pendingApprovalsByTaskId, totalApprovalsByTaskId],
|
||||
);
|
||||
|
||||
const boardAgents = useMemo(
|
||||
() => agents.filter((agent) => !boardId || agent.board_id === boardId),
|
||||
[agents, boardId],
|
||||
);
|
||||
|
||||
const assignableAgents = useMemo(
|
||||
() => boardAgents.filter((agent) => !agent.is_board_lead),
|
||||
[boardAgents],
|
||||
() => agents.filter((agent) => !agent.is_board_lead),
|
||||
[agents],
|
||||
);
|
||||
|
||||
const hasTaskChanges = useMemo(() => {
|
||||
@@ -912,10 +883,7 @@ export default function BoardDetailPage() {
|
||||
const taskApprovals = useMemo(() => {
|
||||
if (!selectedTask) return [];
|
||||
const taskId = selectedTask.id;
|
||||
return approvals.filter((approval) => {
|
||||
const payloadTaskId = approvalTaskId(approval);
|
||||
return payloadTaskId === taskId;
|
||||
});
|
||||
return approvals.filter((approval) => approval.task_id === taskId);
|
||||
}, [approvals, selectedTask]);
|
||||
|
||||
const workingAgentIds = useMemo(() => {
|
||||
@@ -935,12 +903,12 @@ export default function BoardDetailPage() {
|
||||
if (agent.status === "provisioning") return 2;
|
||||
return 3;
|
||||
};
|
||||
return [...boardAgents].sort((a, b) => {
|
||||
return [...agents].sort((a, b) => {
|
||||
const diff = rank(a) - rank(b);
|
||||
if (diff !== 0) return diff;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
}, [boardAgents, workingAgentIds]);
|
||||
}, [agents, workingAgentIds]);
|
||||
|
||||
const loadComments = async (taskId: string) => {
|
||||
if (!isSignedIn || !boardId) return;
|
||||
@@ -953,7 +921,7 @@ export default function BoardDetailPage() {
|
||||
taskId,
|
||||
);
|
||||
if (result.status !== 200) throw new Error("Unable to load comments.");
|
||||
setComments(result.data);
|
||||
setComments(result.data.items ?? []);
|
||||
} catch (err) {
|
||||
setCommentsError(err instanceof Error ? err.message : "Something went wrong.");
|
||||
} finally {
|
||||
@@ -1059,9 +1027,20 @@ export default function BoardDetailPage() {
|
||||
},
|
||||
);
|
||||
if (result.status !== 200) throw new Error("Unable to update task.");
|
||||
const updated = normalizeTask(result.data);
|
||||
const previous =
|
||||
tasksRef.current.find((task) => task.id === selectedTask.id) ??
|
||||
selectedTask;
|
||||
const updated = normalizeTask({
|
||||
...previous,
|
||||
...result.data,
|
||||
assignee: result.data.assigned_agent_id
|
||||
? assigneeById.get(result.data.assigned_agent_id) ?? null
|
||||
: null,
|
||||
approvals_count: previous.approvals_count,
|
||||
approvals_pending_count: previous.approvals_pending_count,
|
||||
} as TaskCardRead);
|
||||
setTasks((prev) =>
|
||||
prev.map((task) => (task.id === updated.id ? updated : task)),
|
||||
prev.map((task) => (task.id === updated.id ? { ...task, ...updated } : task)),
|
||||
);
|
||||
setSelectedTask(updated);
|
||||
if (closeOnSuccess) {
|
||||
@@ -1119,6 +1098,7 @@ export default function BoardDetailPage() {
|
||||
status,
|
||||
assigned_agent_id:
|
||||
status === "inbox" ? null : task.assigned_agent_id,
|
||||
assignee: status === "inbox" ? null : task.assignee,
|
||||
}
|
||||
: task,
|
||||
),
|
||||
@@ -1130,9 +1110,17 @@ export default function BoardDetailPage() {
|
||||
{ status },
|
||||
);
|
||||
if (result.status !== 200) throw new Error("Unable to move task.");
|
||||
const updated = normalizeTask(result.data);
|
||||
const updated = normalizeTask({
|
||||
...currentTask,
|
||||
...result.data,
|
||||
assignee: result.data.assigned_agent_id
|
||||
? assigneeById.get(result.data.assigned_agent_id) ?? null
|
||||
: null,
|
||||
approvals_count: currentTask.approvals_count,
|
||||
approvals_pending_count: currentTask.approvals_pending_count,
|
||||
} as TaskCardRead);
|
||||
setTasks((prev) =>
|
||||
prev.map((task) => (task.id === updated.id ? updated : task)),
|
||||
prev.map((task) => (task.id === updated.id ? { ...task, ...updated } : task)),
|
||||
);
|
||||
} catch (err) {
|
||||
setTasks(previousTasks);
|
||||
@@ -1262,6 +1250,7 @@ export default function BoardDetailPage() {
|
||||
const approvalRows = (approval: Approval) => {
|
||||
const payload = approval.payload ?? {};
|
||||
const taskId =
|
||||
approval.task_id ??
|
||||
approvalPayloadValue(payload, "task_id") ??
|
||||
approvalPayloadValue(payload, "taskId") ??
|
||||
approvalPayloadValue(payload, "taskID");
|
||||
@@ -1499,7 +1488,7 @@ export default function BoardDetailPage() {
|
||||
<>
|
||||
{viewMode === "board" ? (
|
||||
<TaskBoard
|
||||
tasks={displayTasks}
|
||||
tasks={tasks}
|
||||
onTaskSelect={openComments}
|
||||
onTaskMove={handleTaskMove}
|
||||
/>
|
||||
@@ -1512,7 +1501,7 @@ export default function BoardDetailPage() {
|
||||
All tasks
|
||||
</p>
|
||||
<p className="text-xs text-slate-500">
|
||||
{displayTasks.length} tasks in this board
|
||||
{tasks.length} tasks in this board
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
@@ -1526,12 +1515,12 @@ export default function BoardDetailPage() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="divide-y divide-slate-100">
|
||||
{displayTasks.length === 0 ? (
|
||||
{tasks.length === 0 ? (
|
||||
<div className="px-5 py-8 text-sm text-slate-500">
|
||||
No tasks yet. Create your first task to get started.
|
||||
</div>
|
||||
) : (
|
||||
displayTasks.map((task) => (
|
||||
tasks.map((task) => (
|
||||
<button
|
||||
key={task.id}
|
||||
type="button"
|
||||
@@ -1553,10 +1542,10 @@ export default function BoardDetailPage() {
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-3 text-xs text-slate-500">
|
||||
{task.approvalsPendingCount ? (
|
||||
{task.approvals_pending_count ? (
|
||||
<span className="inline-flex items-center gap-2 text-[10px] font-semibold uppercase tracking-wide text-amber-700">
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-amber-500" />
|
||||
Approval needed · {task.approvalsPendingCount}
|
||||
Approval needed · {task.approvals_pending_count}
|
||||
</span>
|
||||
) : null}
|
||||
<span
|
||||
|
||||
@@ -37,7 +37,7 @@ export default function NewBoardPage() {
|
||||
const gatewaysQuery = useListGatewaysApiV1GatewaysGet<
|
||||
listGatewaysApiV1GatewaysGetResponse,
|
||||
ApiError
|
||||
>({
|
||||
>(undefined, {
|
||||
query: {
|
||||
enabled: Boolean(isSignedIn),
|
||||
refetchOnMount: "always",
|
||||
@@ -59,7 +59,9 @@ export default function NewBoardPage() {
|
||||
});
|
||||
|
||||
const gateways =
|
||||
gatewaysQuery.data?.status === 200 ? gatewaysQuery.data.data : [];
|
||||
gatewaysQuery.data?.status === 200
|
||||
? gatewaysQuery.data.data.items ?? []
|
||||
: [];
|
||||
const displayGatewayId = gatewayId || gateways[0]?.id || "";
|
||||
const isLoading = gatewaysQuery.isLoading || createBoardMutation.isPending;
|
||||
const errorMessage = error ?? gatewaysQuery.error?.message ?? null;
|
||||
|
||||
@@ -53,7 +53,7 @@ export default function BoardsPage() {
|
||||
const boardsQuery = useListBoardsApiV1BoardsGet<
|
||||
listBoardsApiV1BoardsGetResponse,
|
||||
ApiError
|
||||
>({
|
||||
>(undefined, {
|
||||
query: {
|
||||
enabled: Boolean(isSignedIn),
|
||||
refetchInterval: 30_000,
|
||||
@@ -62,15 +62,11 @@ export default function BoardsPage() {
|
||||
});
|
||||
|
||||
const boards = useMemo(
|
||||
() => (boardsQuery.data?.status === 200 ? boardsQuery.data.data : []),
|
||||
() =>
|
||||
boardsQuery.data?.status === 200 ? boardsQuery.data.data.items ?? [] : [],
|
||||
[boardsQuery.data]
|
||||
);
|
||||
|
||||
const sortedBoards = useMemo(
|
||||
() => [...boards].sort((a, b) => a.name.localeCompare(b.name)),
|
||||
[boards]
|
||||
);
|
||||
|
||||
const deleteMutation = useDeleteBoardApiV1BoardsBoardIdDelete<
|
||||
ApiError,
|
||||
{ previous?: listBoardsApiV1BoardsGetResponse }
|
||||
@@ -82,9 +78,17 @@ export default function BoardsPage() {
|
||||
const previous =
|
||||
queryClient.getQueryData<listBoardsApiV1BoardsGetResponse>(boardsKey);
|
||||
if (previous && previous.status === 200) {
|
||||
const nextItems = previous.data.items.filter(
|
||||
(board) => board.id !== boardId
|
||||
);
|
||||
const removedCount = previous.data.items.length - nextItems.length;
|
||||
queryClient.setQueryData<listBoardsApiV1BoardsGetResponse>(boardsKey, {
|
||||
...previous,
|
||||
data: previous.data.filter((board) => board.id !== boardId),
|
||||
data: {
|
||||
...previous.data,
|
||||
items: nextItems,
|
||||
total: Math.max(0, previous.data.total - removedCount),
|
||||
},
|
||||
});
|
||||
}
|
||||
return { previous };
|
||||
@@ -159,7 +163,7 @@ export default function BoardsPage() {
|
||||
|
||||
// eslint-disable-next-line react-hooks/incompatible-library
|
||||
const table = useReactTable({
|
||||
data: sortedBoards,
|
||||
data: boards,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
@@ -191,11 +195,11 @@ export default function BoardsPage() {
|
||||
Boards
|
||||
</h1>
|
||||
<p className="mt-1 text-sm text-slate-500">
|
||||
Manage boards and task workflows. {sortedBoards.length} board
|
||||
{sortedBoards.length === 1 ? "" : "s"} total.
|
||||
Manage boards and task workflows. {boards.length} board
|
||||
{boards.length === 1 ? "" : "s"} total.
|
||||
</p>
|
||||
</div>
|
||||
{sortedBoards.length > 0 ? (
|
||||
{boards.length > 0 ? (
|
||||
<Link
|
||||
href="/boards/new"
|
||||
className={buttonVariants({ size: "md", variant: "primary" })}
|
||||
|
||||
@@ -16,11 +16,6 @@ import {
|
||||
type listAgentsApiV1AgentsGetResponse,
|
||||
useListAgentsApiV1AgentsGet,
|
||||
} from "@/api/generated/agents/agents";
|
||||
import {
|
||||
type listBoardsApiV1BoardsGetResponse,
|
||||
useListBoardsApiV1BoardsGet,
|
||||
} from "@/api/generated/boards/boards";
|
||||
import type { AgentRead } from "@/api/generated/model";
|
||||
import { DashboardSidebar } from "@/components/organisms/DashboardSidebar";
|
||||
import { DashboardShell } from "@/components/templates/DashboardShell";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -65,22 +60,12 @@ export default function GatewayDetailPage() {
|
||||
const gateway =
|
||||
gatewayQuery.data?.status === 200 ? gatewayQuery.data.data : null;
|
||||
|
||||
const boardsQuery = useListBoardsApiV1BoardsGet<
|
||||
listBoardsApiV1BoardsGetResponse,
|
||||
ApiError
|
||||
>({
|
||||
query: {
|
||||
enabled: Boolean(isSignedIn),
|
||||
refetchInterval: 30_000,
|
||||
},
|
||||
});
|
||||
|
||||
const agentsQuery = useListAgentsApiV1AgentsGet<
|
||||
listAgentsApiV1AgentsGetResponse,
|
||||
ApiError
|
||||
>({
|
||||
>(gatewayId ? { gateway_id: gatewayId } : undefined, {
|
||||
query: {
|
||||
enabled: Boolean(isSignedIn),
|
||||
enabled: Boolean(isSignedIn && gatewayId),
|
||||
refetchInterval: 15_000,
|
||||
},
|
||||
});
|
||||
@@ -103,22 +88,11 @@ export default function GatewayDetailPage() {
|
||||
},
|
||||
});
|
||||
|
||||
const agents = useMemo(() => {
|
||||
const allAgents = agentsQuery.data?.data ?? [];
|
||||
const boards = boardsQuery.data?.status === 200 ? boardsQuery.data.data : [];
|
||||
if (!gatewayId) {
|
||||
return allAgents;
|
||||
}
|
||||
const boardIds = new Set(
|
||||
boards.filter((board) => board.gateway_id === gatewayId).map((board) => board.id),
|
||||
);
|
||||
if (boardIds.size === 0) {
|
||||
return [];
|
||||
}
|
||||
return allAgents.filter(
|
||||
(agent): agent is AgentRead => Boolean(agent.board_id && boardIds.has(agent.board_id)),
|
||||
);
|
||||
}, [agentsQuery.data, boardsQuery.data, gatewayId]);
|
||||
const agents = useMemo(
|
||||
() =>
|
||||
agentsQuery.data?.status === 200 ? agentsQuery.data.data.items ?? [] : [],
|
||||
[agentsQuery.data],
|
||||
);
|
||||
|
||||
const status =
|
||||
statusQuery.data?.status === 200 ? statusQuery.data.data : null;
|
||||
|
||||
@@ -65,7 +65,7 @@ export default function GatewaysPage() {
|
||||
const gatewaysQuery = useListGatewaysApiV1GatewaysGet<
|
||||
listGatewaysApiV1GatewaysGetResponse,
|
||||
ApiError
|
||||
>({
|
||||
>(undefined, {
|
||||
query: {
|
||||
enabled: Boolean(isSignedIn),
|
||||
refetchInterval: 30_000,
|
||||
@@ -73,7 +73,13 @@ export default function GatewaysPage() {
|
||||
},
|
||||
});
|
||||
|
||||
const gateways = useMemo(() => gatewaysQuery.data?.data ?? [], [gatewaysQuery.data]);
|
||||
const gateways = useMemo(
|
||||
() =>
|
||||
gatewaysQuery.data?.status === 200
|
||||
? gatewaysQuery.data.data.items ?? []
|
||||
: [],
|
||||
[gatewaysQuery.data]
|
||||
);
|
||||
const sortedGateways = useMemo(() => [...gateways], [gateways]);
|
||||
|
||||
const deleteMutation = useDeleteGatewayApiV1GatewaysGatewayIdDelete<
|
||||
@@ -86,10 +92,18 @@ export default function GatewaysPage() {
|
||||
await queryClient.cancelQueries({ queryKey: gatewaysKey });
|
||||
const previous =
|
||||
queryClient.getQueryData<listGatewaysApiV1GatewaysGetResponse>(gatewaysKey);
|
||||
if (previous) {
|
||||
if (previous && previous.status === 200) {
|
||||
const nextItems = previous.data.items.filter(
|
||||
(gateway) => gateway.id !== gatewayId
|
||||
);
|
||||
const removedCount = previous.data.items.length - nextItems.length;
|
||||
queryClient.setQueryData<listGatewaysApiV1GatewaysGetResponse>(gatewaysKey, {
|
||||
...previous,
|
||||
data: previous.data.filter((gateway) => gateway.id !== gatewayId),
|
||||
data: {
|
||||
...previous.data,
|
||||
items: nextItems,
|
||||
total: Math.max(0, previous.data.total - removedCount),
|
||||
},
|
||||
});
|
||||
}
|
||||
return { previous };
|
||||
|
||||
Reference in New Issue
Block a user