fix: normalize custom command handling and improve project settings loading

- Updated the `DevServerService` to normalize custom commands by trimming whitespace and treating empty strings as undefined.
- Refactored the `DevServerSection` component to utilize TanStack Query for fetching project settings, improving data handling and error management.
- Enhanced the save functionality to use mutation hooks for updating project settings, streamlining the save process and ensuring better state management.

These changes enhance the reliability and user experience when configuring development server commands.
This commit is contained in:
Shirone
2026-01-22 17:49:06 +01:00
parent e110c058a2
commit 57ce198ae9
2 changed files with 47 additions and 69 deletions

View File

@@ -358,16 +358,19 @@ class DevServerService {
// Determine the dev command to use // Determine the dev command to use
let devCommand: { cmd: string; args: string[] }; let devCommand: { cmd: string; args: string[] };
if (customCommand) { // Normalize custom command: trim whitespace and treat empty strings as undefined
const normalizedCustomCommand = customCommand?.trim();
if (normalizedCustomCommand) {
// Use the provided custom command // Use the provided custom command
devCommand = this.parseCustomCommand(customCommand); devCommand = this.parseCustomCommand(normalizedCustomCommand);
if (!devCommand.cmd) { if (!devCommand.cmd) {
return { return {
success: false, success: false,
error: 'Invalid custom command: command cannot be empty', error: 'Invalid custom command: command cannot be empty',
}; };
} }
logger.debug(`Using custom command: ${customCommand}`); logger.debug(`Using custom command: ${normalizedCustomCommand}`);
} else { } else {
// Check for package.json when auto-detecting // Check for package.json when auto-detecting
const packageJsonPath = path.join(worktreePath, 'package.json'); const packageJsonPath = path.join(worktreePath, 'package.json');

View File

@@ -5,8 +5,8 @@ import { Button } from '@/components/ui/button';
import { Play, Save, RotateCcw, Info, X } from 'lucide-react'; import { Play, Save, RotateCcw, Info, X } from 'lucide-react';
import { Spinner } from '@/components/ui/spinner'; import { Spinner } from '@/components/ui/spinner';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { getHttpApiClient } from '@/lib/http-api-client'; import { useProjectSettings } from '@/hooks/queries';
import { toast } from 'sonner'; import { useUpdateProjectSettings } from '@/hooks/mutations';
import type { Project } from '@/lib/electron'; import type { Project } from '@/lib/electron';
/** Preset dev server commands for quick selection */ /** Preset dev server commands for quick selection */
@@ -25,77 +25,48 @@ interface DevServerSectionProps {
} }
export function DevServerSection({ project }: DevServerSectionProps) { export function DevServerSection({ project }: DevServerSectionProps) {
// Fetch project settings using TanStack Query
const { data: projectSettings, isLoading, isError } = useProjectSettings(project.path);
// Mutation hook for updating project settings
const updateSettingsMutation = useUpdateProjectSettings(project.path);
// Local state for the input field
const [devCommand, setDevCommand] = useState(''); const [devCommand, setDevCommand] = useState('');
const [originalDevCommand, setOriginalDevCommand] = useState(''); const [originalDevCommand, setOriginalDevCommand] = useState('');
const [isLoading, setIsLoading] = useState(true);
const [isSaving, setIsSaving] = useState(false);
// Check if there are unsaved changes // Sync local state when project settings load or project changes
const hasChanges = devCommand !== originalDevCommand;
// Load project settings when project changes
useEffect(() => { useEffect(() => {
let isCancelled = false; // Reset local state when project changes to avoid showing stale values
const currentPath = project.path; setDevCommand('');
setOriginalDevCommand('');
}, [project.path]);
const loadProjectSettings = async () => { useEffect(() => {
setIsLoading(true); if (projectSettings) {
try { const command = projectSettings.devCommand || '';
const httpClient = getHttpApiClient();
const response = await httpClient.settings.getProject(currentPath);
// Avoid updating state if component unmounted or project changed
if (isCancelled) return;
if (response.success && response.settings) {
const command = response.settings.devCommand || '';
setDevCommand(command); setDevCommand(command);
setOriginalDevCommand(command); setOriginalDevCommand(command);
} }
} catch (error) { }, [projectSettings]);
if (!isCancelled) {
console.error('Failed to load project settings:', error);
}
} finally {
if (!isCancelled) {
setIsLoading(false);
}
}
};
loadProjectSettings(); // Check if there are unsaved changes
const hasChanges = devCommand !== originalDevCommand;
return () => { const isSaving = updateSettingsMutation.isPending;
isCancelled = true;
};
}, [project.path]);
// Save dev command // Save dev command
const handleSave = useCallback(async () => { const handleSave = useCallback(() => {
setIsSaving(true);
try {
const httpClient = getHttpApiClient();
const normalizedCommand = devCommand.trim(); const normalizedCommand = devCommand.trim();
const response = await httpClient.settings.updateProject(project.path, { updateSettingsMutation.mutate(
devCommand: normalizedCommand || undefined, { devCommand: normalizedCommand || undefined },
}); {
onSuccess: () => {
if (response.success) {
setDevCommand(normalizedCommand); setDevCommand(normalizedCommand);
setOriginalDevCommand(normalizedCommand); setOriginalDevCommand(normalizedCommand);
toast.success('Dev server command saved'); },
} else {
toast.error('Failed to save dev server command', {
description: response.error,
});
} }
} catch (error) { );
console.error('Failed to save dev server command:', error); }, [devCommand, updateSettingsMutation]);
toast.error('Failed to save dev server command');
} finally {
setIsSaving(false);
}
}, [project.path, devCommand]);
// Reset to original value // Reset to original value
const handleReset = useCallback(() => { const handleReset = useCallback(() => {
@@ -151,6 +122,10 @@ export function DevServerSection({ project }: DevServerSectionProps) {
<div className="flex items-center justify-center py-8"> <div className="flex items-center justify-center py-8">
<Spinner size="md" /> <Spinner size="md" />
</div> </div>
) : isError ? (
<div className="flex items-center justify-center py-8 text-sm text-destructive">
Failed to load project settings. Please try again.
</div>
) : ( ) : (
<> <>
{/* Dev Command Input */} {/* Dev Command Input */}
@@ -179,7 +154,7 @@ export function DevServerSection({ project }: DevServerSectionProps) {
size="sm" size="sm"
onClick={handleClear} onClick={handleClear}
className="absolute right-1 top-1/2 -translate-y-1/2 h-6 w-6 p-0 text-muted-foreground hover:text-foreground" className="absolute right-1 top-1/2 -translate-y-1/2 h-6 w-6 p-0 text-muted-foreground hover:text-foreground"
title="Clear to use auto-detection" aria-label="Clear dev command"
> >
<X className="w-3.5 h-3.5" /> <X className="w-3.5 h-3.5" />
</Button> </Button>