From d66630a43f979a3a55fd304a3913f5fd744e838d Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Tue, 9 Jun 2026 06:25:16 -0700 Subject: [PATCH] FN-6073: preserve pending workflow value edits during save Preserve unsaved workflow value edits made while a save request is in flight. - track the keys included in the current save request before sending it - clear only the saved keys after a successful save so newer edits remain pending - add regression coverage for mid-flight edits that trigger a follow-up save Files changed: packages/dashboard/app/components/WorkflowSettingsPanel.tsx | 7 +++- packages/dashboard/app/components/__tests__/WorkflowSettingsPanel.test.tsx | 41 +++++++++++++++++++++- 2 files changed, 46 insertions(+), 2 deletions(-) Fusion-Task-Id: FN-6073 Fusion-Task-Lineage: 31e05269-d51d-40c0-9691-87e61d464992 --- .../app/components/WorkflowSettingsPanel.tsx | 7 +++- .../__tests__/WorkflowSettingsPanel.test.tsx | 41 ++++++++++++++++++- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/packages/dashboard/app/components/WorkflowSettingsPanel.tsx b/packages/dashboard/app/components/WorkflowSettingsPanel.tsx index 04bcf7727b..0822548b5a 100644 --- a/packages/dashboard/app/components/WorkflowSettingsPanel.tsx +++ b/packages/dashboard/app/components/WorkflowSettingsPanel.tsx @@ -721,11 +721,16 @@ function ValuesTab({ const save = useCallback(async () => { if (!dirty) return; + const savedKeys = new Set(Object.keys(pending)); setSaving(true); try { const res = await updateWorkflowSettingValues(workflowId, pending, boundProjectId); setPayload(res); - setPending({}); + setPending((prev) => { + const next = { ...prev }; + for (const k of savedKeys) delete next[k]; + return next; + }); setRejections({}); addToast(t("workflowSettings.valuesSaved", "Setting values saved"), "success"); } catch (err) { diff --git a/packages/dashboard/app/components/__tests__/WorkflowSettingsPanel.test.tsx b/packages/dashboard/app/components/__tests__/WorkflowSettingsPanel.test.tsx index 72a132bd78..4115c62b48 100644 --- a/packages/dashboard/app/components/__tests__/WorkflowSettingsPanel.test.tsx +++ b/packages/dashboard/app/components/__tests__/WorkflowSettingsPanel.test.tsx @@ -9,7 +9,7 @@ * `updateWorkflowSettingValues`). */ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; -import { render, screen, fireEvent, waitFor, cleanup, within } from "@testing-library/react"; +import { render, screen, fireEvent, waitFor, cleanup, within, act } from "@testing-library/react"; import { useState } from "react"; import * as jestDomMatchers from "@testing-library/jest-dom/matchers"; @@ -227,6 +227,45 @@ describe("WorkflowSettingsPanel — Values tab", () => { ); }); + it("preserves mid-flight edits after a successful save and resaves only the new keys", async () => { + mockFetchValues.mockResolvedValue(payload({ effective: { "timeout-ms": 1000, "new-sessions": false, label: "" } })); + let resolveSave!: (value: WorkflowSettingValuesPayload) => void; + mockUpdateValues + .mockReturnValueOnce( + new Promise((resolve) => { + resolveSave = resolve; + }), + ) + .mockResolvedValueOnce( + payload({ + stored: { "timeout-ms": 5000, label: "mid-flight-edit" }, + effective: { "timeout-ms": 5000, "new-sessions": false, label: "mid-flight-edit" }, + }), + ); + + render(); + openValues(); + await waitFor(() => expect(mockFetchValues).toHaveBeenCalled()); + + fireEvent.change(screen.getByLabelText("Timeout"), { target: { value: "5000" } }); + fireEvent.click(screen.getByTestId("wf-settings-save-values")); + await waitFor(() => expect(mockUpdateValues).toHaveBeenCalledTimes(1)); + + fireEvent.change(screen.getByLabelText("Label"), { target: { value: "mid-flight-edit" } }); + await act(async () => { + resolveSave(payload({ stored: { "timeout-ms": 5000 }, effective: { "timeout-ms": 5000, "new-sessions": false } })); + }); + + expect(mockUpdateValues).toHaveBeenNthCalledWith(1, "wf-1", { "timeout-ms": 5000 }, "proj-1"); + expect(screen.getByLabelText("Label")).toHaveValue("mid-flight-edit"); + expect(screen.getByLabelText("Timeout")).toHaveValue(5000); + + await waitFor(() => expect(screen.getByTestId("wf-settings-save-values")).not.toBeDisabled()); + fireEvent.click(screen.getByTestId("wf-settings-save-values")); + await waitFor(() => expect(mockUpdateValues).toHaveBeenCalledTimes(2)); + expect(mockUpdateValues).toHaveBeenNthCalledWith(2, "wf-1", { label: "mid-flight-edit" }, "proj-1"); + }); + it("renders a per-field rejection on the matching row and keeps other edits applied", async () => { mockFetchValues.mockResolvedValue(payload({ effective: { "timeout-ms": 1000, "new-sessions": false } })); mockUpdateValues.mockRejectedValueOnce(