mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
refactor: consolidate dev and test command configuration into a new CommandsSection
- Introduced a new `CommandsSection` component to manage both development and test commands, replacing the previous `DevServerSection` and `TestingSection`. - Updated the `SettingsService` to handle special cases for `devCommand` and `testCommand`, allowing for null values to delete commands. - Removed deprecated sections and streamlined the project settings view to enhance user experience and maintainability. This refactor simplifies command management and improves the overall structure of the project settings interface.
This commit is contained in:
@@ -837,6 +837,20 @@ export class SettingsService {
|
||||
delete updated.defaultFeatureModel;
|
||||
}
|
||||
|
||||
// Handle devCommand special cases:
|
||||
// - null means delete the key (use auto-detection)
|
||||
// - string means custom command
|
||||
if ('devCommand' in updates && updates.devCommand === null) {
|
||||
delete updated.devCommand;
|
||||
}
|
||||
|
||||
// Handle testCommand special cases:
|
||||
// - null means delete the key (use auto-detection)
|
||||
// - string means custom command
|
||||
if ('testCommand' in updates && updates.testCommand === null) {
|
||||
delete updated.testCommand;
|
||||
}
|
||||
|
||||
await writeSettingsJson(settingsPath, updated);
|
||||
logger.info(`Project settings updated for ${projectPath}`);
|
||||
|
||||
|
||||
@@ -0,0 +1,316 @@
|
||||
import { useState, useEffect, useCallback, type KeyboardEvent } from 'react';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Terminal, Save, RotateCcw, Info, X, Play, FlaskConical } from 'lucide-react';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useProjectSettings } from '@/hooks/queries';
|
||||
import { useUpdateProjectSettings } from '@/hooks/mutations';
|
||||
import type { Project } from '@/lib/electron';
|
||||
|
||||
/** Preset dev server commands for quick selection */
|
||||
const DEV_SERVER_PRESETS = [
|
||||
{ label: 'npm run dev', command: 'npm run dev' },
|
||||
{ label: 'yarn dev', command: 'yarn dev' },
|
||||
{ label: 'pnpm dev', command: 'pnpm dev' },
|
||||
{ label: 'bun dev', command: 'bun dev' },
|
||||
{ label: 'npm start', command: 'npm start' },
|
||||
{ label: 'cargo watch', command: 'cargo watch -x run' },
|
||||
{ label: 'go run', command: 'go run .' },
|
||||
] as const;
|
||||
|
||||
/** Preset test commands for quick selection */
|
||||
const TEST_PRESETS = [
|
||||
{ label: 'npm test', command: 'npm test' },
|
||||
{ label: 'yarn test', command: 'yarn test' },
|
||||
{ label: 'pnpm test', command: 'pnpm test' },
|
||||
{ label: 'bun test', command: 'bun test' },
|
||||
{ label: 'pytest', command: 'pytest' },
|
||||
{ label: 'cargo test', command: 'cargo test' },
|
||||
{ label: 'go test', command: 'go test ./...' },
|
||||
] as const;
|
||||
|
||||
interface CommandsSectionProps {
|
||||
project: Project;
|
||||
}
|
||||
|
||||
export function CommandsSection({ project }: CommandsSectionProps) {
|
||||
// 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 fields
|
||||
const [devCommand, setDevCommand] = useState('');
|
||||
const [originalDevCommand, setOriginalDevCommand] = useState('');
|
||||
const [testCommand, setTestCommand] = useState('');
|
||||
const [originalTestCommand, setOriginalTestCommand] = useState('');
|
||||
|
||||
// Sync local state when project settings load or project changes
|
||||
useEffect(() => {
|
||||
// Reset local state when project changes to avoid showing stale values
|
||||
setDevCommand('');
|
||||
setOriginalDevCommand('');
|
||||
setTestCommand('');
|
||||
setOriginalTestCommand('');
|
||||
}, [project.path]);
|
||||
|
||||
useEffect(() => {
|
||||
if (projectSettings) {
|
||||
const dev = projectSettings.devCommand || '';
|
||||
const test = projectSettings.testCommand || '';
|
||||
setDevCommand(dev);
|
||||
setOriginalDevCommand(dev);
|
||||
setTestCommand(test);
|
||||
setOriginalTestCommand(test);
|
||||
}
|
||||
}, [projectSettings]);
|
||||
|
||||
// Check if there are unsaved changes
|
||||
const hasDevChanges = devCommand !== originalDevCommand;
|
||||
const hasTestChanges = testCommand !== originalTestCommand;
|
||||
const hasChanges = hasDevChanges || hasTestChanges;
|
||||
const isSaving = updateSettingsMutation.isPending;
|
||||
|
||||
// Save all commands
|
||||
const handleSave = useCallback(() => {
|
||||
const normalizedDevCommand = devCommand.trim();
|
||||
const normalizedTestCommand = testCommand.trim();
|
||||
|
||||
updateSettingsMutation.mutate(
|
||||
{
|
||||
devCommand: normalizedDevCommand || null,
|
||||
testCommand: normalizedTestCommand || null,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
setDevCommand(normalizedDevCommand);
|
||||
setOriginalDevCommand(normalizedDevCommand);
|
||||
setTestCommand(normalizedTestCommand);
|
||||
setOriginalTestCommand(normalizedTestCommand);
|
||||
},
|
||||
}
|
||||
);
|
||||
}, [devCommand, testCommand, updateSettingsMutation]);
|
||||
|
||||
// Reset to original values
|
||||
const handleReset = useCallback(() => {
|
||||
setDevCommand(originalDevCommand);
|
||||
setTestCommand(originalTestCommand);
|
||||
}, [originalDevCommand, originalTestCommand]);
|
||||
|
||||
// Use a preset command
|
||||
const handleUseDevPreset = useCallback((command: string) => {
|
||||
setDevCommand(command);
|
||||
}, []);
|
||||
|
||||
const handleUseTestPreset = useCallback((command: string) => {
|
||||
setTestCommand(command);
|
||||
}, []);
|
||||
|
||||
// Clear commands
|
||||
const handleClearDev = useCallback(() => {
|
||||
setDevCommand('');
|
||||
}, []);
|
||||
|
||||
const handleClearTest = useCallback(() => {
|
||||
setTestCommand('');
|
||||
}, []);
|
||||
|
||||
// Handle keyboard shortcuts (Enter to save)
|
||||
const handleKeyDown = useCallback(
|
||||
(e: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter' && hasChanges && !isSaving) {
|
||||
e.preventDefault();
|
||||
handleSave();
|
||||
}
|
||||
},
|
||||
[hasChanges, isSaving, handleSave]
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'rounded-2xl overflow-hidden',
|
||||
'border border-border/50',
|
||||
'bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl',
|
||||
'shadow-sm shadow-black/5'
|
||||
)}
|
||||
>
|
||||
<div className="p-6 border-b border-border/50 bg-gradient-to-r from-transparent via-accent/5 to-transparent">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-600/10 flex items-center justify-center border border-brand-500/20">
|
||||
<Terminal className="w-5 h-5 text-brand-500" />
|
||||
</div>
|
||||
<h2 className="text-lg font-semibold text-foreground tracking-tight">Project Commands</h2>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground/80 ml-12">
|
||||
Configure custom commands for development and testing.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-6 space-y-8">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Spinner size="md" />
|
||||
</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 Server Command Section */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Play className="w-4 h-4 text-brand-500" />
|
||||
<h3 className="text-base font-medium text-foreground">Dev Server</h3>
|
||||
{hasDevChanges && (
|
||||
<span className="text-xs text-amber-500 font-medium">(unsaved)</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 pl-6">
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="dev-command"
|
||||
value={devCommand}
|
||||
onChange={(e) => setDevCommand(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="e.g., npm run dev, yarn dev, cargo watch"
|
||||
className="font-mono text-sm pr-8"
|
||||
data-testid="dev-command-input"
|
||||
/>
|
||||
{devCommand && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleClearDev}
|
||||
className="absolute right-1 top-1/2 -translate-y-1/2 h-6 w-6 p-0 text-muted-foreground hover:text-foreground"
|
||||
aria-label="Clear dev command"
|
||||
>
|
||||
<X className="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground/80">
|
||||
Leave empty to auto-detect based on your package manager.
|
||||
</p>
|
||||
|
||||
{/* Dev Presets */}
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{DEV_SERVER_PRESETS.map((preset) => (
|
||||
<Button
|
||||
key={preset.command}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleUseDevPreset(preset.command)}
|
||||
className="text-xs font-mono h-7 px-2"
|
||||
>
|
||||
{preset.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="border-t border-border/30" />
|
||||
|
||||
{/* Test Command Section */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<FlaskConical className="w-4 h-4 text-brand-500" />
|
||||
<h3 className="text-base font-medium text-foreground">Test Runner</h3>
|
||||
{hasTestChanges && (
|
||||
<span className="text-xs text-amber-500 font-medium">(unsaved)</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 pl-6">
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="test-command"
|
||||
value={testCommand}
|
||||
onChange={(e) => setTestCommand(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="e.g., npm test, pytest, cargo test"
|
||||
className="font-mono text-sm pr-8"
|
||||
data-testid="test-command-input"
|
||||
/>
|
||||
{testCommand && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleClearTest}
|
||||
className="absolute right-1 top-1/2 -translate-y-1/2 h-6 w-6 p-0 text-muted-foreground hover:text-foreground"
|
||||
aria-label="Clear test command"
|
||||
>
|
||||
<X className="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground/80">
|
||||
Leave empty to auto-detect based on your project structure.
|
||||
</p>
|
||||
|
||||
{/* Test Presets */}
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{TEST_PRESETS.map((preset) => (
|
||||
<Button
|
||||
key={preset.command}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleUseTestPreset(preset.command)}
|
||||
className="text-xs font-mono h-7 px-2"
|
||||
>
|
||||
{preset.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Auto-detection Info */}
|
||||
<div className="flex items-start gap-3 p-3 rounded-lg bg-accent/20 border border-border/30">
|
||||
<Info className="w-4 h-4 text-brand-500 mt-0.5 shrink-0" />
|
||||
<div className="text-xs text-muted-foreground">
|
||||
<p className="font-medium text-foreground mb-1">Auto-detection</p>
|
||||
<p>
|
||||
When no custom command is set, the system automatically detects your package
|
||||
manager and test framework based on project files (package.json, Cargo.toml,
|
||||
go.mod, etc.).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex items-center justify-end gap-2 pt-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleReset}
|
||||
disabled={!hasChanges || isSaving}
|
||||
className="gap-1.5"
|
||||
>
|
||||
<RotateCcw className="w-3.5 h-3.5" />
|
||||
Reset
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleSave}
|
||||
disabled={!hasChanges || isSaving}
|
||||
className="gap-1.5"
|
||||
>
|
||||
{isSaving ? <Spinner size="xs" /> : <Save className="w-3.5 h-3.5" />}
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -6,8 +6,7 @@ import {
|
||||
AlertTriangle,
|
||||
Workflow,
|
||||
Database,
|
||||
FlaskConical,
|
||||
Play,
|
||||
Terminal,
|
||||
} from 'lucide-react';
|
||||
import type { ProjectSettingsViewId } from '../hooks/use-project-settings-view';
|
||||
|
||||
@@ -20,8 +19,7 @@ export interface ProjectNavigationItem {
|
||||
export const PROJECT_SETTINGS_NAV_ITEMS: ProjectNavigationItem[] = [
|
||||
{ id: 'identity', label: 'Identity', icon: User },
|
||||
{ id: 'worktrees', label: 'Worktrees', icon: GitBranch },
|
||||
{ id: 'testing', label: 'Testing', icon: FlaskConical },
|
||||
{ id: 'devServer', label: 'Dev Server', icon: Play },
|
||||
{ id: 'commands', label: 'Commands', icon: Terminal },
|
||||
{ id: 'theme', label: 'Theme', icon: Palette },
|
||||
{ id: 'claude', label: 'Models', icon: Workflow },
|
||||
{ id: 'data', label: 'Data', icon: Database },
|
||||
|
||||
@@ -1,231 +0,0 @@
|
||||
import { useState, useEffect, useCallback, type KeyboardEvent } from 'react';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Play, Save, RotateCcw, Info, X } from 'lucide-react';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useProjectSettings } from '@/hooks/queries';
|
||||
import { useUpdateProjectSettings } from '@/hooks/mutations';
|
||||
import type { Project } from '@/lib/electron';
|
||||
|
||||
/** Preset dev server commands for quick selection */
|
||||
const DEV_SERVER_PRESETS = [
|
||||
{ label: 'npm run dev', command: 'npm run dev' },
|
||||
{ label: 'yarn dev', command: 'yarn dev' },
|
||||
{ label: 'pnpm dev', command: 'pnpm dev' },
|
||||
{ label: 'bun dev', command: 'bun dev' },
|
||||
{ label: 'npm start', command: 'npm start' },
|
||||
{ label: 'cargo watch', command: 'cargo watch -x run' },
|
||||
{ label: 'go run', command: 'go run .' },
|
||||
] as const;
|
||||
|
||||
interface DevServerSectionProps {
|
||||
project: Project;
|
||||
}
|
||||
|
||||
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 [originalDevCommand, setOriginalDevCommand] = useState('');
|
||||
|
||||
// Sync local state when project settings load or project changes
|
||||
useEffect(() => {
|
||||
// Reset local state when project changes to avoid showing stale values
|
||||
setDevCommand('');
|
||||
setOriginalDevCommand('');
|
||||
}, [project.path]);
|
||||
|
||||
useEffect(() => {
|
||||
if (projectSettings) {
|
||||
const command = projectSettings.devCommand || '';
|
||||
setDevCommand(command);
|
||||
setOriginalDevCommand(command);
|
||||
}
|
||||
}, [projectSettings]);
|
||||
|
||||
// Check if there are unsaved changes
|
||||
const hasChanges = devCommand !== originalDevCommand;
|
||||
const isSaving = updateSettingsMutation.isPending;
|
||||
|
||||
// Save dev command
|
||||
const handleSave = useCallback(() => {
|
||||
const normalizedCommand = devCommand.trim();
|
||||
updateSettingsMutation.mutate(
|
||||
{ devCommand: normalizedCommand || undefined },
|
||||
{
|
||||
onSuccess: () => {
|
||||
setDevCommand(normalizedCommand);
|
||||
setOriginalDevCommand(normalizedCommand);
|
||||
},
|
||||
}
|
||||
);
|
||||
}, [devCommand, updateSettingsMutation]);
|
||||
|
||||
// Reset to original value
|
||||
const handleReset = useCallback(() => {
|
||||
setDevCommand(originalDevCommand);
|
||||
}, [originalDevCommand]);
|
||||
|
||||
// Use a preset command
|
||||
const handleUsePreset = useCallback((command: string) => {
|
||||
setDevCommand(command);
|
||||
}, []);
|
||||
|
||||
// Clear the command to use auto-detection
|
||||
const handleClear = useCallback(() => {
|
||||
setDevCommand('');
|
||||
}, []);
|
||||
|
||||
// Handle keyboard shortcuts (Enter to save)
|
||||
const handleKeyDown = useCallback(
|
||||
(e: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter' && hasChanges && !isSaving) {
|
||||
e.preventDefault();
|
||||
handleSave();
|
||||
}
|
||||
},
|
||||
[hasChanges, isSaving, handleSave]
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'rounded-2xl overflow-hidden',
|
||||
'border border-border/50',
|
||||
'bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl',
|
||||
'shadow-sm shadow-black/5'
|
||||
)}
|
||||
>
|
||||
<div className="p-6 border-b border-border/50 bg-gradient-to-r from-transparent via-accent/5 to-transparent">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-600/10 flex items-center justify-center border border-brand-500/20">
|
||||
<Play className="w-5 h-5 text-brand-500" />
|
||||
</div>
|
||||
<h2 className="text-lg font-semibold text-foreground tracking-tight">
|
||||
Dev Server Configuration
|
||||
</h2>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground/80 ml-12">
|
||||
Configure how the development server is started for this project.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-6 space-y-6">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Spinner size="md" />
|
||||
</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 */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="dev-command" className="text-foreground font-medium">
|
||||
Dev Server Command
|
||||
</Label>
|
||||
{hasChanges && (
|
||||
<span className="text-xs text-amber-500 font-medium">(unsaved changes)</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="dev-command"
|
||||
value={devCommand}
|
||||
onChange={(e) => setDevCommand(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="e.g., npm run dev, yarn dev, cargo watch, go run ."
|
||||
className="font-mono text-sm pr-8"
|
||||
data-testid="dev-command-input"
|
||||
/>
|
||||
{devCommand && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
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"
|
||||
aria-label="Clear dev command"
|
||||
>
|
||||
<X className="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground/80 leading-relaxed">
|
||||
The command to start the development server for this project. If not specified, the
|
||||
system will auto-detect based on your package manager (npm/yarn/pnpm/bun run dev).
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Auto-detection Info */}
|
||||
<div className="flex items-start gap-3 p-3 rounded-lg bg-accent/20 border border-border/30">
|
||||
<Info className="w-4 h-4 text-brand-500 mt-0.5 shrink-0" />
|
||||
<div className="text-xs text-muted-foreground">
|
||||
<p className="font-medium text-foreground mb-1">Auto-detection</p>
|
||||
<p>
|
||||
When no custom command is set, the dev server automatically detects your package
|
||||
manager (npm, yarn, pnpm, or bun) and runs the "dev" script. Set a
|
||||
custom command if your project uses a different script name (e.g., start, serve)
|
||||
or requires additional flags.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Presets */}
|
||||
<div className="space-y-3">
|
||||
<Label className="text-foreground font-medium">Quick Presets</Label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{DEV_SERVER_PRESETS.map((preset) => (
|
||||
<Button
|
||||
key={preset.command}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleUsePreset(preset.command)}
|
||||
className="text-xs font-mono"
|
||||
>
|
||||
{preset.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground/80">
|
||||
Click a preset to use it as your dev server command. Press Enter to save.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex items-center justify-end gap-2 pt-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleReset}
|
||||
disabled={!hasChanges || isSaving}
|
||||
className="gap-1.5"
|
||||
>
|
||||
<RotateCcw className="w-3.5 h-3.5" />
|
||||
Reset
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleSave}
|
||||
disabled={!hasChanges || isSaving}
|
||||
className="gap-1.5"
|
||||
>
|
||||
{isSaving ? <Spinner size="xs" /> : <Save className="w-3.5 h-3.5" />}
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -4,8 +4,7 @@ export type ProjectSettingsViewId =
|
||||
| 'identity'
|
||||
| 'theme'
|
||||
| 'worktrees'
|
||||
| 'testing'
|
||||
| 'devServer'
|
||||
| 'commands'
|
||||
| 'claude'
|
||||
| 'data'
|
||||
| 'danger';
|
||||
|
||||
@@ -2,6 +2,6 @@ export { ProjectSettingsView } from './project-settings-view';
|
||||
export { ProjectIdentitySection } from './project-identity-section';
|
||||
export { ProjectThemeSection } from './project-theme-section';
|
||||
export { WorktreePreferencesSection } from './worktree-preferences-section';
|
||||
export { TestingSection } from './testing-section';
|
||||
export { CommandsSection } from './commands-section';
|
||||
export { useProjectSettingsView, type ProjectSettingsViewId } from './hooks';
|
||||
export { ProjectSettingsNavigation } from './components/project-settings-navigation';
|
||||
|
||||
@@ -5,8 +5,7 @@ import { Button } from '@/components/ui/button';
|
||||
import { ProjectIdentitySection } from './project-identity-section';
|
||||
import { ProjectThemeSection } from './project-theme-section';
|
||||
import { WorktreePreferencesSection } from './worktree-preferences-section';
|
||||
import { TestingSection } from './testing-section';
|
||||
import { DevServerSection } from './dev-server-section';
|
||||
import { CommandsSection } from './commands-section';
|
||||
import { ProjectModelsSection } from './project-models-section';
|
||||
import { DataManagementSection } from './data-management-section';
|
||||
import { DangerZoneSection } from '../settings-view/danger-zone/danger-zone-section';
|
||||
@@ -88,10 +87,8 @@ export function ProjectSettingsView() {
|
||||
return <ProjectThemeSection project={currentProject} />;
|
||||
case 'worktrees':
|
||||
return <WorktreePreferencesSection project={currentProject} />;
|
||||
case 'testing':
|
||||
return <TestingSection project={currentProject} />;
|
||||
case 'devServer':
|
||||
return <DevServerSection project={currentProject} />;
|
||||
case 'commands':
|
||||
return <CommandsSection project={currentProject} />;
|
||||
case 'claude':
|
||||
return <ProjectModelsSection project={currentProject} />;
|
||||
case 'data':
|
||||
|
||||
@@ -1,223 +0,0 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { FlaskConical, Save, RotateCcw, Info } from 'lucide-react';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { getHttpApiClient } from '@/lib/http-api-client';
|
||||
import { toast } from 'sonner';
|
||||
import type { Project } from '@/lib/electron';
|
||||
|
||||
interface TestingSectionProps {
|
||||
project: Project;
|
||||
}
|
||||
|
||||
export function TestingSection({ project }: TestingSectionProps) {
|
||||
const [testCommand, setTestCommand] = useState('');
|
||||
const [originalTestCommand, setOriginalTestCommand] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
// Check if there are unsaved changes
|
||||
const hasChanges = testCommand !== originalTestCommand;
|
||||
|
||||
// Load project settings when project changes
|
||||
useEffect(() => {
|
||||
let isCancelled = false;
|
||||
const currentPath = project.path;
|
||||
|
||||
const loadProjectSettings = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
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.testCommand || '';
|
||||
setTestCommand(command);
|
||||
setOriginalTestCommand(command);
|
||||
}
|
||||
} catch (error) {
|
||||
if (!isCancelled) {
|
||||
console.error('Failed to load project settings:', error);
|
||||
}
|
||||
} finally {
|
||||
if (!isCancelled) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loadProjectSettings();
|
||||
|
||||
return () => {
|
||||
isCancelled = true;
|
||||
};
|
||||
}, [project.path]);
|
||||
|
||||
// Save test command
|
||||
const handleSave = useCallback(async () => {
|
||||
setIsSaving(true);
|
||||
try {
|
||||
const httpClient = getHttpApiClient();
|
||||
const normalizedCommand = testCommand.trim();
|
||||
const response = await httpClient.settings.updateProject(project.path, {
|
||||
testCommand: normalizedCommand || undefined,
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
setTestCommand(normalizedCommand);
|
||||
setOriginalTestCommand(normalizedCommand);
|
||||
toast.success('Test command saved');
|
||||
} else {
|
||||
toast.error('Failed to save test command', {
|
||||
description: response.error,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to save test command:', error);
|
||||
toast.error('Failed to save test command');
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}, [project.path, testCommand]);
|
||||
|
||||
// Reset to original value
|
||||
const handleReset = useCallback(() => {
|
||||
setTestCommand(originalTestCommand);
|
||||
}, [originalTestCommand]);
|
||||
|
||||
// Use a preset command
|
||||
const handleUsePreset = useCallback((command: string) => {
|
||||
setTestCommand(command);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'rounded-2xl overflow-hidden',
|
||||
'border border-border/50',
|
||||
'bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl',
|
||||
'shadow-sm shadow-black/5'
|
||||
)}
|
||||
>
|
||||
<div className="p-6 border-b border-border/50 bg-gradient-to-r from-transparent via-accent/5 to-transparent">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-600/10 flex items-center justify-center border border-brand-500/20">
|
||||
<FlaskConical className="w-5 h-5 text-brand-500" />
|
||||
</div>
|
||||
<h2 className="text-lg font-semibold text-foreground tracking-tight">
|
||||
Testing Configuration
|
||||
</h2>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground/80 ml-12">
|
||||
Configure how tests are run for this project.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-6 space-y-6">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Spinner size="md" />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Test Command Input */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="test-command" className="text-foreground font-medium">
|
||||
Test Command
|
||||
</Label>
|
||||
{hasChanges && (
|
||||
<span className="text-xs text-amber-500 font-medium">(unsaved changes)</span>
|
||||
)}
|
||||
</div>
|
||||
<Input
|
||||
id="test-command"
|
||||
value={testCommand}
|
||||
onChange={(e) => setTestCommand(e.target.value)}
|
||||
placeholder="e.g., npm test, yarn test, pytest, go test ./..."
|
||||
className="font-mono text-sm"
|
||||
data-testid="test-command-input"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground/80 leading-relaxed">
|
||||
The command to run tests for this project. If not specified, the test runner will
|
||||
auto-detect based on your project structure (package.json, Cargo.toml, go.mod,
|
||||
etc.).
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Auto-detection Info */}
|
||||
<div className="flex items-start gap-3 p-3 rounded-lg bg-accent/20 border border-border/30">
|
||||
<Info className="w-4 h-4 text-brand-500 mt-0.5 shrink-0" />
|
||||
<div className="text-xs text-muted-foreground">
|
||||
<p className="font-medium text-foreground mb-1">Auto-detection</p>
|
||||
<p>
|
||||
When no custom command is set, the test runner automatically detects and uses the
|
||||
appropriate test framework based on your project files (Vitest, Jest, Pytest,
|
||||
Cargo, Go Test, etc.).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Presets */}
|
||||
<div className="space-y-3">
|
||||
<Label className="text-foreground font-medium">Quick Presets</Label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{[
|
||||
{ label: 'npm test', command: 'npm test' },
|
||||
{ label: 'yarn test', command: 'yarn test' },
|
||||
{ label: 'pnpm test', command: 'pnpm test' },
|
||||
{ label: 'bun test', command: 'bun test' },
|
||||
{ label: 'pytest', command: 'pytest' },
|
||||
{ label: 'cargo test', command: 'cargo test' },
|
||||
{ label: 'go test', command: 'go test ./...' },
|
||||
].map((preset) => (
|
||||
<Button
|
||||
key={preset.command}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleUsePreset(preset.command)}
|
||||
className="text-xs font-mono"
|
||||
>
|
||||
{preset.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground/80">
|
||||
Click a preset to use it as your test command.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex items-center justify-end gap-2 pt-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleReset}
|
||||
disabled={!hasChanges || isSaving}
|
||||
className="gap-1.5"
|
||||
>
|
||||
<RotateCcw className="w-3.5 h-3.5" />
|
||||
Reset
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleSave}
|
||||
disabled={!hasChanges || isSaving}
|
||||
className="gap-1.5"
|
||||
>
|
||||
{isSaving ? <Spinner size="xs" /> : <Save className="w-3.5 h-3.5" />}
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user