fix(dashboard): scope goals view to selected project
This commit is contained in:
7
.changeset/fix-goals-project-scope.md
Normal file
7
.changeset/fix-goals-project-scope.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Keep the Goals dashboard view scoped to the selected project.
|
||||
category: fix
|
||||
dev: Threads projectId through Goals view reads, mutations, mission links, and AI description drafting.
|
||||
@@ -11,6 +11,7 @@ import "./GoalsView.css";
|
||||
export interface GoalsViewProps {
|
||||
initialGoals?: Goal[];
|
||||
anchorGoalId?: string;
|
||||
projectId?: string;
|
||||
onNavigateToMission?: (missionId: string) => void;
|
||||
}
|
||||
|
||||
@@ -25,11 +26,17 @@ const WARNING_THRESHOLD = 3;
|
||||
|
||||
const GOAL_DESCRIPTION_TOGGLE_LENGTH = 280;
|
||||
|
||||
function withProjectId(path: string, projectId?: string): string {
|
||||
if (!projectId) return path;
|
||||
const separator = path.includes("?") ? "&" : "?";
|
||||
return `${path}${separator}projectId=${encodeURIComponent(projectId)}`;
|
||||
}
|
||||
|
||||
function isCapError(payload: unknown): boolean {
|
||||
return Boolean(payload && typeof payload === "object" && "code" in payload && (payload as { code?: unknown }).code === "ACTIVE_GOAL_LIMIT_EXCEEDED");
|
||||
}
|
||||
|
||||
export function GoalsView({ initialGoals, anchorGoalId, onNavigateToMission }: GoalsViewProps) {
|
||||
export function GoalsView({ initialGoals, anchorGoalId, projectId, onNavigateToMission }: GoalsViewProps) {
|
||||
const { t } = useTranslation("app");
|
||||
const [goals, setGoals] = useState<Goal[]>(() => initialGoals ?? []);
|
||||
const [highlightedGoalId, setHighlightedGoalId] = useState<string | null>(null);
|
||||
@@ -66,7 +73,7 @@ export function GoalsView({ initialGoals, anchorGoalId, onNavigateToMission }: G
|
||||
try {
|
||||
setLoading(true);
|
||||
setErrorMessage(null);
|
||||
const response = await fetch("/api/goals");
|
||||
const response = await fetch(withProjectId("/api/goals", projectId));
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load goals (${response.status})`);
|
||||
}
|
||||
@@ -93,13 +100,13 @@ export function GoalsView({ initialGoals, anchorGoalId, onNavigateToMission }: G
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [initialGoals]);
|
||||
}, [initialGoals, projectId]);
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
const loadMissions = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/missions");
|
||||
const response = await fetch(withProjectId("/api/missions", projectId));
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load missions (${response.status})`);
|
||||
}
|
||||
@@ -124,10 +131,10 @@ export function GoalsView({ initialGoals, anchorGoalId, onNavigateToMission }: G
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [t]);
|
||||
}, [projectId, t]);
|
||||
|
||||
const loadLinkedMissionsForGoal = async (goalId: string): Promise<LinkedMission[]> => {
|
||||
const response = await fetch(`/api/goals/${encodeURIComponent(goalId)}/missions`);
|
||||
const response = await fetch(withProjectId(`/api/goals/${encodeURIComponent(goalId)}/missions`, projectId));
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load linked missions (${response.status})`);
|
||||
}
|
||||
@@ -235,7 +242,7 @@ export function GoalsView({ initialGoals, anchorGoalId, onNavigateToMission }: G
|
||||
try {
|
||||
setIsDraftingDescription(true);
|
||||
setAddError(null);
|
||||
const description = await draftGoalDescription(title);
|
||||
const description = await draftGoalDescription(title, projectId);
|
||||
setAddDescription(description);
|
||||
} catch (error) {
|
||||
setAddError(getRefineErrorMessage(error));
|
||||
@@ -255,7 +262,7 @@ export function GoalsView({ initialGoals, anchorGoalId, onNavigateToMission }: G
|
||||
setIsCreating(true);
|
||||
setAddError(null);
|
||||
setErrorMessage(null);
|
||||
const response = await fetch("/api/goals", {
|
||||
const response = await fetch(withProjectId("/api/goals", projectId), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
@@ -307,7 +314,7 @@ export function GoalsView({ initialGoals, anchorGoalId, onNavigateToMission }: G
|
||||
try {
|
||||
setIsSavingEdit(true);
|
||||
setEditError(null);
|
||||
const response = await fetch(`/api/goals/${editGoalId}`, {
|
||||
const response = await fetch(withProjectId(`/api/goals/${editGoalId}`, projectId), {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
@@ -368,7 +375,7 @@ export function GoalsView({ initialGoals, anchorGoalId, onNavigateToMission }: G
|
||||
try {
|
||||
setLinkingMissionGoalId(goalId);
|
||||
setErrorMessage(null);
|
||||
const response = await fetch(`/api/missions/${encodeURIComponent(missionId)}/goals/${encodeURIComponent(goalId)}`, { method: "POST" });
|
||||
const response = await fetch(withProjectId(`/api/missions/${encodeURIComponent(missionId)}/goals/${encodeURIComponent(goalId)}`, projectId), { method: "POST" });
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to link mission (${response.status})`);
|
||||
}
|
||||
@@ -384,7 +391,7 @@ export function GoalsView({ initialGoals, anchorGoalId, onNavigateToMission }: G
|
||||
try {
|
||||
setUnlinkingMissionKey(`${goalId}:${missionId}`);
|
||||
setErrorMessage(null);
|
||||
const response = await fetch(`/api/missions/${encodeURIComponent(missionId)}/goals/${encodeURIComponent(goalId)}`, { method: "DELETE" });
|
||||
const response = await fetch(withProjectId(`/api/missions/${encodeURIComponent(missionId)}/goals/${encodeURIComponent(goalId)}`, projectId), { method: "DELETE" });
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to unlink mission (${response.status})`);
|
||||
}
|
||||
@@ -401,7 +408,7 @@ export function GoalsView({ initialGoals, anchorGoalId, onNavigateToMission }: G
|
||||
|
||||
try {
|
||||
setErrorMessage(null);
|
||||
const response = await fetch(endpoint, {
|
||||
const response = await fetch(withProjectId(endpoint, projectId), {
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
|
||||
@@ -182,6 +182,30 @@ describe("GoalsView", () => {
|
||||
expect(await screen.findByText("Loaded Goal")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("threads projectId through goals and mission reads", async () => {
|
||||
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
|
||||
const path = String(input);
|
||||
if (path === "/api/goals?projectId=proj-fusion") {
|
||||
return { ok: true, json: async () => ({ goals: [makeGoal({ id: "g1", title: "Fusion Goal" })] }) };
|
||||
}
|
||||
if (path === "/api/missions?projectId=proj-fusion") {
|
||||
return { ok: true, json: async () => ({ missions: [] }) };
|
||||
}
|
||||
if (path === "/api/goals/g1/missions?projectId=proj-fusion") {
|
||||
return { ok: true, json: async () => ({ missions: [] }) };
|
||||
}
|
||||
return { ok: false, status: 404, json: async () => ({}) };
|
||||
});
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
render(<GoalsView projectId="proj-fusion" />);
|
||||
|
||||
expect(await screen.findByText("Fusion Goal")).toBeInTheDocument();
|
||||
await waitFor(() => expect(fetchMock).toHaveBeenCalledWith("/api/goals?projectId=proj-fusion"));
|
||||
expect(fetchMock).toHaveBeenCalledWith("/api/missions?projectId=proj-fusion");
|
||||
expect(fetchMock).toHaveBeenCalledWith("/api/goals/g1/missions?projectId=proj-fusion");
|
||||
});
|
||||
|
||||
it("renders inline load error when API request fails", async () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
@@ -388,7 +412,7 @@ describe("GoalsView", () => {
|
||||
fireEvent.change(screen.getByTestId("goals-form-title"), { target: { value: "Grow ecosystem" } });
|
||||
fireEvent.click(screen.getByTestId("goals-form-draft-ai"));
|
||||
|
||||
await waitFor(() => expect(mockDraftGoalDescription).toHaveBeenCalledWith("Grow ecosystem"));
|
||||
await waitFor(() => expect(mockDraftGoalDescription).toHaveBeenCalledWith("Grow ecosystem", undefined));
|
||||
expect(screen.getByTestId("goals-form-description")).toHaveValue(
|
||||
"Expand the extension ecosystem with better support and adoption goals."
|
||||
);
|
||||
|
||||
@@ -531,7 +531,7 @@ export function MainContent({
|
||||
return (
|
||||
<PageErrorBoundary>
|
||||
<Suspense fallback={null}>
|
||||
<GoalsView anchorGoalId={goalAnchorId} onNavigateToMission={handleOpenMission} />
|
||||
<GoalsView anchorGoalId={goalAnchorId} projectId={currentProject?.id} onNavigateToMission={handleOpenMission} />
|
||||
</Suspense>
|
||||
</PageErrorBoundary>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user