FN-5751: fix dashboard auto-merge toggle race
Prevent rapid auto-merge toggles from desynchronizing dashboard settings state. - Track auto-merge state in a ref so each toggle reads and writes the latest value - Apply optimistic updates from the ref and roll back correctly on failed setting updates - Add regression coverage for rapid double-toggle behavior in useAppSettings - Add a patch changeset for @runfusion/fusion describing the dashboard blank-state fix Files changed: .changeset/fn-5751-auto-merge-toggle.md | 5 ++++ .../app/hooks/__tests__/useAppSettings.test.ts | 27 ++++++++++++++++++++++ packages/dashboard/app/hooks/useAppSettings.ts | 20 +++++++++++----- 3 files changed, 46 insertions(+), 6 deletions(-) Fusion-Task-Id: FN-5751 Fusion-Task-Lineage: 1a0090ff-4015-46b8-a974-3accbbdce919
This commit is contained in:
5
.changeset/fn-5751-auto-merge-toggle.md
Normal file
5
.changeset/fn-5751-auto-merge-toggle.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
Fixes a dashboard regression where toggling the in-review Auto-merge switch could leave the UI in a broken/blank state until refresh. Auto-merge toggle state updates now remain consistent during rapid toggles, and regression coverage was added for the settings hook path.
|
||||
@@ -75,6 +75,33 @@ describe("useAppSettings", () => {
|
||||
expect(mockUpdateSettings).toHaveBeenCalledWith({ autoMerge: true }, "proj_123");
|
||||
});
|
||||
|
||||
it("preserves consistent autoMerge state across rapid toggles", async () => {
|
||||
const updateResolvers: Array<() => void> = [];
|
||||
mockUpdateSettings.mockImplementation(
|
||||
() => new Promise((resolve) => updateResolvers.push(() => resolve({} as never))),
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useAppSettings("proj_123"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.autoMerge).toBe(false);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
const firstToggle = result.current.toggleAutoMerge();
|
||||
const secondToggle = result.current.toggleAutoMerge();
|
||||
|
||||
expect(result.current.autoMerge).toBe(false);
|
||||
|
||||
updateResolvers.forEach((resolve) => resolve());
|
||||
await Promise.all([firstToggle, secondToggle]);
|
||||
});
|
||||
|
||||
expect(mockUpdateSettings).toHaveBeenNthCalledWith(1, { autoMerge: true }, "proj_123");
|
||||
expect(mockUpdateSettings).toHaveBeenNthCalledWith(2, { autoMerge: false }, "proj_123");
|
||||
expect(result.current.autoMerge).toBe(false);
|
||||
});
|
||||
|
||||
it("rolls back optimistic state when toggle update fails", async () => {
|
||||
mockUpdateSettings.mockRejectedValueOnce(new Error("network"));
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { fetchConfig, fetchSettings, updateSettings, updateGlobalSettings } from "../api";
|
||||
import { setAutoReloadEnabled } from "../versionCheck";
|
||||
|
||||
@@ -63,6 +63,7 @@ export function useAppSettings(projectId?: string): UseAppSettingsResult {
|
||||
const [todosEnabled, setTodosEnabled] = useState(false);
|
||||
const [goalsEnabled, setGoalsEnabled] = useState(false);
|
||||
const [autoReloadOnVersionChange, setAutoReloadOnVersionChangeState] = useState(true);
|
||||
const autoMergeRef = useRef(autoMerge);
|
||||
|
||||
/**
|
||||
* Fetches config and settings from the backend and updates local state.
|
||||
@@ -124,16 +125,23 @@ export function useAppSettings(projectId?: string): UseAppSettingsResult {
|
||||
void refresh();
|
||||
}, [refresh]);
|
||||
|
||||
useEffect(() => {
|
||||
autoMergeRef.current = autoMerge;
|
||||
}, [autoMerge]);
|
||||
|
||||
const toggleAutoMerge = useCallback(async () => {
|
||||
const next = !autoMerge;
|
||||
setAutoMerge(next);
|
||||
const previousAutoMerge = autoMergeRef.current;
|
||||
const nextAutoMerge = !previousAutoMerge;
|
||||
autoMergeRef.current = nextAutoMerge;
|
||||
setAutoMerge(nextAutoMerge);
|
||||
|
||||
try {
|
||||
await updateSettings({ autoMerge: next }, projectId);
|
||||
await updateSettings({ autoMerge: nextAutoMerge }, projectId);
|
||||
} catch {
|
||||
setAutoMerge(!next);
|
||||
autoMergeRef.current = previousAutoMerge;
|
||||
setAutoMerge(previousAutoMerge);
|
||||
}
|
||||
}, [autoMerge, projectId]);
|
||||
}, [projectId]);
|
||||
|
||||
const toggleGlobalPause = useCallback(async () => {
|
||||
const next = !globalPaused;
|
||||
|
||||
Reference in New Issue
Block a user