fix: scope all git, activity, webhook, and summarize-title routes to correct project store
- Fix 30 git routes to use getScopedStore(req) instead of global store - Fix activity GET/DELETE routes to use scoped store - Fix POST /api/github/webhooks to use scoped store for badge updates - Fix POST /api/ai/summarize-title to use scoped store for settings - Fix GET /api/git/worktrees to use scoped store for task listing - Add projectId parameter to all git and activity frontend API functions - Update useActivityLog hook to pass projectId through Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { memo, useCallback, useMemo, useState } from "react";
|
||||
import { Activity, Server, Settings, Trash2 } from "lucide-react";
|
||||
import type { NodeInfo, ProjectInfo } from "../api";
|
||||
import { getProjectCountForNode } from "../utils/nodeProjectAssignment";
|
||||
|
||||
export interface NodeCardProps {
|
||||
node: NodeInfo;
|
||||
@@ -23,10 +24,6 @@ function truncateUrl(url: string, maxLength: number = 42): string {
|
||||
return `${url.slice(0, maxLength - 3)}...`;
|
||||
}
|
||||
|
||||
function getAssignedProjectCount(projects: ProjectInfo[], nodeId: string): number {
|
||||
return projects.filter((project) => project.nodeId === nodeId).length;
|
||||
}
|
||||
|
||||
function areNodeCardPropsEqual(previous: NodeCardProps, next: NodeCardProps): boolean {
|
||||
const prevNode = previous.node;
|
||||
const nextNode = next.node;
|
||||
@@ -40,8 +37,9 @@ function areNodeCardPropsEqual(previous: NodeCardProps, next: NodeCardProps): bo
|
||||
if (prevNode.updatedAt !== nextNode.updatedAt) return false;
|
||||
if (previous.isLoading !== next.isLoading) return false;
|
||||
|
||||
const previousCount = getAssignedProjectCount(previous.projects, prevNode.id);
|
||||
const nextCount = getAssignedProjectCount(next.projects, nextNode.id);
|
||||
// Compare project counts using the canonical counting function
|
||||
const previousCount = getProjectCountForNode(previous.projects, prevNode);
|
||||
const nextCount = getProjectCountForNode(next.projects, nextNode);
|
||||
return previousCount === nextCount;
|
||||
}
|
||||
|
||||
@@ -57,8 +55,8 @@ function NodeCardInner({
|
||||
const statusConfig = STATUS_CONFIG[node.status];
|
||||
|
||||
const assignedProjectCount = useMemo(() => {
|
||||
return getAssignedProjectCount(projects, node.id);
|
||||
}, [projects, node.id]);
|
||||
return getProjectCountForNode(projects, node);
|
||||
}, [projects, node]);
|
||||
|
||||
const handleOpenDetails = useCallback(() => {
|
||||
onEdit(node);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Activity, Pencil, Save, X } from "lucide-react";
|
||||
import type { NodeInfo, NodeUpdateInput, ProjectInfo } from "../api";
|
||||
import type { ToastType } from "../hooks/useToast";
|
||||
import { getProjectsForNode } from "../utils/nodeProjectAssignment";
|
||||
|
||||
interface NodeDetailModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -65,7 +66,7 @@ export function NodeDetailModal({
|
||||
|
||||
const assignedProjects = useMemo(() => {
|
||||
if (!node) return [];
|
||||
return projects.filter((project) => project.nodeId === node.id);
|
||||
return getProjectsForNode(projects, node);
|
||||
}, [node, projects]);
|
||||
|
||||
const handleHealthCheck = useCallback(async () => {
|
||||
@@ -244,9 +245,13 @@ export function NodeDetailModal({
|
||||
</section>
|
||||
|
||||
<section className="node-detail-modal__section">
|
||||
<h4>Assigned Projects ({assignedProjects.length})</h4>
|
||||
<h4>{node.type === "local" ? "Projects" : "Assigned Projects"} ({assignedProjects.length})</h4>
|
||||
{assignedProjects.length === 0 ? (
|
||||
<p className="node-detail-modal__empty">No projects are assigned to this node.</p>
|
||||
<p className="node-detail-modal__empty">
|
||||
{node.type === "local"
|
||||
? "No projects are running on this node."
|
||||
: "No projects are assigned to this node."}
|
||||
</p>
|
||||
) : (
|
||||
<ul className="node-detail-modal__project-list">
|
||||
{assignedProjects.map((project) => (
|
||||
|
||||
@@ -144,4 +144,48 @@ describe("NodeCard", () => {
|
||||
fireEvent.click(screen.getByLabelText("Confirm remove node"));
|
||||
expect(onRemove).toHaveBeenCalledWith(node.id);
|
||||
});
|
||||
|
||||
it("local node counts include unassigned projects", () => {
|
||||
const localNode = makeNode({ id: "local-1", type: "local" });
|
||||
const projects = [
|
||||
makeProject({ id: "proj-1", nodeId: "local-1" }), // explicitly assigned
|
||||
makeProject({ id: "proj-2", nodeId: undefined }), // unassigned - runs on local
|
||||
makeProject({ id: "proj-3", nodeId: "remote-1" }), // assigned to remote - not counted
|
||||
];
|
||||
|
||||
render(
|
||||
<NodeCard
|
||||
node={localNode}
|
||||
projects={projects}
|
||||
onHealthCheck={vi.fn()}
|
||||
onEdit={vi.fn()}
|
||||
onRemove={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
// Local node should show 2 projects (explicitly assigned + unassigned)
|
||||
expect(screen.getByText("2")).toBeDefined();
|
||||
});
|
||||
|
||||
it("remote node counts exclude unassigned projects", () => {
|
||||
const remoteNode = makeNode({ id: "remote-1", type: "remote" });
|
||||
const projects = [
|
||||
makeProject({ id: "proj-1", nodeId: "remote-1" }), // explicitly assigned
|
||||
makeProject({ id: "proj-2", nodeId: undefined }), // unassigned - NOT counted for remote
|
||||
makeProject({ id: "proj-3", nodeId: "local-1" }), // assigned to local - not counted
|
||||
];
|
||||
|
||||
render(
|
||||
<NodeCard
|
||||
node={remoteNode}
|
||||
projects={projects}
|
||||
onHealthCheck={vi.fn()}
|
||||
onEdit={vi.fn()}
|
||||
onRemove={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
// Remote node should show only 1 project (explicitly assigned only)
|
||||
expect(screen.getByText("1")).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -148,4 +148,33 @@ describe("NodesView", () => {
|
||||
fireEvent.click(nodeCard!);
|
||||
expect(screen.getByRole("dialog", { name: "Node details for Detail Node" })).toBeDefined();
|
||||
});
|
||||
|
||||
it("local node project count includes unassigned projects in detail modal", () => {
|
||||
mockUseProjects.mockReturnValue({
|
||||
projects: [
|
||||
makeProject({ id: "proj-1", nodeId: "node-1" }), // explicitly assigned
|
||||
makeProject({ id: "proj-2", nodeId: undefined }), // unassigned - runs on local
|
||||
],
|
||||
loading: false,
|
||||
error: null,
|
||||
refresh: vi.fn().mockResolvedValue(undefined),
|
||||
register: vi.fn(),
|
||||
update: vi.fn(),
|
||||
unregister: vi.fn(),
|
||||
});
|
||||
|
||||
mockUseNodes.mockReturnValue(makeUseNodesResult({
|
||||
nodes: [makeNode({ id: "node-1", name: "Local Node", type: "local" })],
|
||||
}));
|
||||
|
||||
render(<NodesView addToast={vi.fn()} />);
|
||||
|
||||
// Click on the node card to open detail modal
|
||||
const nodeCard = document.querySelector(".node-card");
|
||||
expect(nodeCard).toBeInTheDocument();
|
||||
fireEvent.click(nodeCard!);
|
||||
|
||||
// Modal should show "Projects (2)" - including the unassigned project
|
||||
expect(screen.getByText("Projects (2)")).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user