feat(FN-1613): merge fusion/fn-1613

This commit is contained in:
gsxdsm
2026-04-12 08:25:17 -07:00
parent 8f0bcf7f00
commit 28e0061b2f
4 changed files with 74 additions and 22 deletions

View File

@@ -136,6 +136,7 @@ function AppInner() {
toggleAutoMerge,
toggleGlobalPause,
toggleEnginePause,
refresh: refreshAppSettings,
} = useAppSettings(currentProject?.id);
const {
availableModels,
@@ -542,6 +543,10 @@ function AppInner() {
taskOperations={{ moveTask, deleteTask, mergeTask, retryTask, duplicateTask }}
deepLink={{ handleDetailClose }}
settings={{ githubTokenConfigured, themeMode, colorTheme, setThemeMode, setColorTheme }}
onSettingsClose={() => {
modalManager.closeSettings();
void refreshAppSettings();
}}
/>
</>
);

View File

@@ -53,6 +53,8 @@ interface AppModalsProps {
setThemeMode: (mode: ThemeMode) => void;
setColorTheme: (theme: ColorTheme) => void;
};
/** Optional override for the settings modal close handler. When provided, this is called instead of modalManager.closeSettings. */
onSettingsClose?: () => void;
}
export function AppModals({
@@ -69,7 +71,11 @@ export function AppModals({
taskOperations,
deepLink,
settings,
onSettingsClose,
}: AppModalsProps) {
// Use the override handler if provided, otherwise fall back to modalManager.closeSettings
const handleSettingsClose = onSettingsClose ?? modalManager.closeSettings;
return (
<>
{modalManager.detailTask && (
@@ -96,7 +102,7 @@ export function AppModals({
{modalManager.settingsOpen && (
<ModalErrorBoundary>
<SettingsModal
onClose={modalManager.closeSettings}
onClose={handleSettingsClose}
addToast={addToast}
initialSection={modalManager.settingsInitialSection}
projectId={projectId}

View File

@@ -83,4 +83,36 @@ describe("useAppSettings", () => {
expect(result.current.globalPaused).toBe(true);
expect(mockUpdateSettings).toHaveBeenCalledWith({ globalPause: false }, "proj_123");
});
it("refresh() re-fetches and updates state", async () => {
const { result } = renderHook(() => useAppSettings("proj_123"));
// Initial state from first mock
await waitFor(() => {
expect(result.current.showQuickChatFAB).toBe(false);
});
// Change mock to return different value
mockFetchSettings.mockResolvedValueOnce({
autoMerge: false,
globalPause: true,
enginePaused: false,
githubTokenConfigured: true,
taskStuckTimeoutMs: 600000,
showQuickChatFAB: true,
} as never);
// Call refresh
await act(async () => {
await result.current.refresh();
});
// Verify state was updated
await waitFor(() => {
expect(result.current.showQuickChatFAB).toBe(true);
});
// Verify fetchSettings was called again with correct projectId
expect(mockFetchSettings).toHaveBeenCalledWith("proj_123");
});
});

View File

@@ -17,6 +17,8 @@ export interface UseAppSettingsResult {
toggleGlobalPause: () => Promise<void>;
toggleEnginePause: () => Promise<void>;
toggleShowQuickChatFAB: () => Promise<void>;
/** Re-fetches settings from the backend to pick up changes made externally (e.g., by SettingsModal). */
refresh: () => Promise<void>;
}
/**
@@ -32,30 +34,36 @@ export function useAppSettings(projectId?: string): UseAppSettingsResult {
const [showQuickChatFAB, setShowQuickChatFAB] = useState(true);
const [githubTokenConfigured, setGithubTokenConfigured] = useState(false);
useEffect(() => {
fetchConfig(projectId)
.then((cfg) => {
setMaxConcurrent(cfg.maxConcurrent);
setRootDir(cfg.rootDir);
})
.catch(() => {
// Keep defaults on fetch failure.
});
/**
* Fetches config and settings from the backend and updates local state.
* Shared between the mount-time useEffect and the refresh() function.
*/
const refresh = useCallback(async () => {
try {
const cfg = await fetchConfig(projectId);
setMaxConcurrent(cfg.maxConcurrent);
setRootDir(cfg.rootDir);
} catch {
// Keep current state on fetch failure.
}
fetchSettings(projectId)
.then((settings) => {
setAutoMerge(Boolean(settings.autoMerge));
setGlobalPaused(Boolean(settings.globalPause));
setEnginePaused(Boolean(settings.enginePaused));
setGithubTokenConfigured(Boolean(settings.githubTokenConfigured));
setTaskStuckTimeoutMs(settings.taskStuckTimeoutMs);
setShowQuickChatFAB(settings.showQuickChatFAB !== false);
})
.catch(() => {
// Keep defaults on fetch failure.
});
try {
const settings = await fetchSettings(projectId);
setAutoMerge(Boolean(settings.autoMerge));
setGlobalPaused(Boolean(settings.globalPause));
setEnginePaused(Boolean(settings.enginePaused));
setGithubTokenConfigured(Boolean(settings.githubTokenConfigured));
setTaskStuckTimeoutMs(settings.taskStuckTimeoutMs);
setShowQuickChatFAB(settings.showQuickChatFAB !== false);
} catch {
// Keep current state on fetch failure.
}
}, [projectId]);
useEffect(() => {
void refresh();
}, [refresh]);
const toggleAutoMerge = useCallback(async () => {
const next = !autoMerge;
setAutoMerge(next);
@@ -113,5 +121,6 @@ export function useAppSettings(projectId?: string): UseAppSettingsResult {
toggleGlobalPause,
toggleEnginePause,
toggleShowQuickChatFAB,
refresh,
};
}