diff --git a/.changeset/fn-7964-project-models-workflow-lanes.md b/.changeset/fn-7964-project-models-workflow-lanes.md new file mode 100644 index 0000000000..3948bc4623 --- /dev/null +++ b/.changeset/fn-7964-project-models-workflow-lanes.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Fix Project Models workflow model lane saves. +category: fix +dev: Keeps pending default-workflow model lane overrides registered for Settings Save after section navigation. diff --git a/packages/dashboard/app/components/SettingsModal.tsx b/packages/dashboard/app/components/SettingsModal.tsx index 92311eff92..2b4696eb42 100644 --- a/packages/dashboard/app/components/SettingsModal.tsx +++ b/packages/dashboard/app/components/SettingsModal.tsx @@ -999,7 +999,13 @@ export function SettingsModal({ const settingsContentRef = useRef(null); const workflowLaneSaverRef = useRef(null); const registerWorkflowLaneSaver = useCallback((saver: SectionSaveHandler | null) => { - workflowLaneSaverRef.current = saver; + /* + FNXC:ProjectModelsWorkflowLanes 2026-07-14-09:07: + Project Models workflow lane edits are workflow setting-values, not normal project settings. Keep the latest saver registered across section unmounts so the primary Settings Save still flushes project-scoped workflow overrides when operators navigate away before saving. + */ + if (saver) { + workflowLaneSaverRef.current = saver; + } }, []); // Modal-only: persist user-resized dialog dimensions. Embedded view fills its host and is not resizable. useModalResizePersist(modalRef, resizePersistEnabled, "fusion:settings-modal-size"); diff --git a/packages/dashboard/app/components/__tests__/SettingsModal.models-auth.test.tsx b/packages/dashboard/app/components/__tests__/SettingsModal.models-auth.test.tsx index 42936ff867..f9a0f73312 100644 --- a/packages/dashboard/app/components/__tests__/SettingsModal.models-auth.test.tsx +++ b/packages/dashboard/app/components/__tests__/SettingsModal.models-auth.test.tsx @@ -482,6 +482,34 @@ describe("SettingsModal", () => { expect(onClose).toHaveBeenCalled(); }); + it("renders saved workflow model lane values as project overrides after reload", async () => { + const expectedPatch = { planningProvider: "openai", planningModelId: "gpt-4o" }; + mockUpdateWorkflowSettingValues.mockResolvedValue({ + stored: expectedPatch, + effective: expectedPatch, + orphaned: [], + }); + await setupWorkflowModelLaneTest(); + + await settingsModalUser.click(screen.getByLabelText("Plan/Triage Model")); + await settingsModalUser.click(await screen.findByText("GPT-4o")); + await settingsModalUser.click(screen.getByRole("button", { name: "Save" })); + await waitFor(() => { + expect(mockUpdateWorkflowSettingValues).toHaveBeenCalledWith("workflow-custom", expectedPatch, "proj-1"); + }); + + cleanup(); + mockFetchWorkflow.mockClear(); + mockFetchWorkflowSettingValues.mockClear(); + mockUpdateWorkflowSettingValues.mockClear(); + await setupWorkflowModelLaneTest({ stored: expectedPatch, effective: expectedPatch }); + + const lane = screen.getByTestId("workflow-model-lane-planning"); + expect(within(lane).getByText("Override (Project)")).toBeInTheDocument(); + expect(within(lane).getByText("GPT-4o")).toBeInTheDocument(); + expect(screen.getByTestId("workflow-model-lane-execution")).toHaveTextContent("Inherited (Workflow)"); + }); + it("renders fallback workflow model lanes only when the default workflow declares them", async () => { await setupWorkflowModelLaneTest(); @@ -566,6 +594,36 @@ describe("SettingsModal", () => { expect(mockUpdateWorkflowSettingValues).not.toHaveBeenCalled(); }); + it("preserves pending workflow lane edits when Project Models unmounts before primary Save", async () => { + const expectedPatch = { planningProvider: "openai", planningModelId: "gpt-4o" }; + mockUpdateWorkflowSettingValues.mockResolvedValue({ + stored: expectedPatch, + effective: expectedPatch, + orphaned: [], + }); + const onClose = vi.fn(); + await setupWorkflowModelLaneTest({ renderProps: { onClose } }); + + await settingsModalUser.click(screen.getByLabelText("Plan/Triage Model")); + await settingsModalUser.click(await screen.findByText("GPT-4o")); + expect(within(screen.getByTestId("workflow-model-lane-planning")).getByText("GPT-4o")).toBeInTheDocument(); + + await settingsModalUser.click(screen.getByRole("button", { name: "General" })); + await waitFor(() => { + expect(screen.queryByTestId("workflow-model-lane-planning")).not.toBeInTheDocument(); + }); + await settingsModalUser.click(screen.getByRole("button", { name: "Save" })); + + await waitFor(() => { + expect(mockUpdateWorkflowSettingValues).toHaveBeenCalledWith( + "workflow-custom", + expectedPatch, + "proj-1", + ); + }); + expect(onClose).toHaveBeenCalled(); + }); + it("resets workflow model lanes by sending null patches from the primary Settings Save", async () => { await setupWorkflowModelLaneTest({ stored: { executionProvider: "anthropic", executionModelId: "claude-sonnet-4-5" },