Files
fusion/packages/dashboard/app/hooks/useDevServerConfig.ts
Fusion 42476ec3da feat(FN-2166): persist dev server script configuration across sessions
- Extend dev server store with config defaults, normalization, and JSON persistence alongside runtime state.
- Add GET/PUT /api/dev-server/config endpoints with strict request validation for nullable fields and preview URLs.
- Add dashboard API helpers plus a useDevServerConfig hook to load and update selected script, source, command, and preview override.
- Update DevServerView and styles to support saved script selection, change/clear actions, and synchronized command/preview inputs.
- Expand dev server store/routes/component tests and document the config endpoint in architecture docs.
2026-04-20 12:59:58 -07:00

126 lines
3.1 KiB
TypeScript

import { useCallback, useEffect, useRef, useState } from "react";
import {
fetchDevServerConfig,
saveDevServerConfig,
type DevServerConfig,
} from "../api";
interface SelectableDevServerScript {
name: string;
command: string;
source: string;
}
export interface UseDevServerConfigResult {
config: DevServerConfig | null;
loading: boolean;
error: string | null;
selectScript: (script: SelectableDevServerScript) => Promise<void>;
clearSelection: () => Promise<void>;
setPreviewUrlOverride: (url: string | null) => Promise<void>;
refresh: () => Promise<void>;
}
function normalizeError(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
export function useDevServerConfig(projectId?: string): UseDevServerConfigResult {
const [config, setConfig] = useState<DevServerConfig | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const versionRef = useRef(0);
const refresh = useCallback(async () => {
const version = versionRef.current + 1;
versionRef.current = version;
setLoading(true);
try {
const nextConfig = await fetchDevServerConfig(projectId);
if (versionRef.current !== version) {
return;
}
setConfig(nextConfig);
setError(null);
} catch (refreshError) {
if (versionRef.current !== version) {
return;
}
setError(normalizeError(refreshError));
} finally {
if (versionRef.current === version) {
setLoading(false);
}
}
}, [projectId]);
const savePartial = useCallback(async (partial: Partial<DevServerConfig>) => {
const version = versionRef.current + 1;
versionRef.current = version;
setLoading(true);
setError(null);
try {
const updated = await saveDevServerConfig(partial, projectId);
if (versionRef.current !== version) {
return;
}
setConfig(updated);
} catch (saveError) {
if (versionRef.current !== version) {
return;
}
const message = normalizeError(saveError);
setError(message);
throw saveError;
} finally {
if (versionRef.current === version) {
setLoading(false);
}
}
}, [projectId]);
const selectScript = useCallback(async (script: SelectableDevServerScript) => {
await savePartial({
selectedScript: script.name,
selectedSource: script.source,
selectedCommand: script.command,
selectedAt: new Date().toISOString(),
});
}, [savePartial]);
const clearSelection = useCallback(async () => {
await savePartial({
selectedScript: null,
selectedSource: null,
selectedCommand: null,
selectedAt: null,
});
}, [savePartial]);
const setPreviewUrlOverride = useCallback(async (url: string | null) => {
await savePartial({
previewUrlOverride: url,
});
}, [savePartial]);
useEffect(() => {
void refresh();
}, [refresh]);
return {
config,
loading,
error,
selectScript,
clearSelection,
setPreviewUrlOverride,
refresh,
};
}