mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-02 20:43:36 +00:00
feat: Enhance ShellSyntaxEditor and WorktreesSection with new features
This commit introduces several improvements to the ShellSyntaxEditor and WorktreesSection components: 1. **ShellSyntaxEditor**: Added a `maxHeight` prop to allow for customizable maximum height, enhancing layout flexibility. 2. **WorktreesSection**: - Introduced state management for original script content and existence checks for scripts. - Implemented save, reset, and delete functionalities for initialization scripts, providing users with better control over their scripts. - Added action buttons for saving, resetting, and deleting scripts, along with loading indicators for improved user feedback. - Enhanced UI to indicate unsaved changes, improving user awareness of script modifications. These changes improve the user experience by providing more robust script management capabilities and a more responsive UI.
This commit is contained in:
@@ -13,6 +13,7 @@ interface ShellSyntaxEditorProps {
|
|||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
minHeight?: string;
|
minHeight?: string;
|
||||||
|
maxHeight?: string;
|
||||||
'data-testid'?: string;
|
'data-testid'?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,14 +109,12 @@ export function ShellSyntaxEditor({
|
|||||||
placeholder,
|
placeholder,
|
||||||
className,
|
className,
|
||||||
minHeight = '200px',
|
minHeight = '200px',
|
||||||
|
maxHeight,
|
||||||
'data-testid': testId,
|
'data-testid': testId,
|
||||||
}: ShellSyntaxEditorProps) {
|
}: ShellSyntaxEditorProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn('w-full rounded-lg border border-border bg-muted/30', className)}
|
||||||
'w-full rounded-lg border border-border bg-muted/30 overflow-hidden',
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
style={{ minHeight }}
|
style={{ minHeight }}
|
||||||
data-testid={testId}
|
data-testid={testId}
|
||||||
>
|
>
|
||||||
@@ -125,7 +124,9 @@ export function ShellSyntaxEditor({
|
|||||||
extensions={extensions}
|
extensions={extensions}
|
||||||
theme="none"
|
theme="none"
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
className="h-full [&_.cm-editor]:h-full [&_.cm-editor]:min-h-[inherit]"
|
height={maxHeight}
|
||||||
|
minHeight={minHeight}
|
||||||
|
className="[&_.cm-editor]:min-h-[inherit]"
|
||||||
basicSetup={{
|
basicSetup={{
|
||||||
lineNumbers: true,
|
lineNumbers: true,
|
||||||
foldGutter: false,
|
foldGutter: false,
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
import { ShellSyntaxEditor } from '@/components/ui/shell-syntax-editor';
|
import { ShellSyntaxEditor } from '@/components/ui/shell-syntax-editor';
|
||||||
import { GitBranch, Terminal, FileCode, Check, Loader2 } from 'lucide-react';
|
import { GitBranch, Terminal, FileCode, Save, RotateCcw, Trash2, Loader2 } from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { apiPost, apiPut } from '@/lib/api-fetch';
|
import { apiPost, apiPut, apiDelete } from '@/lib/api-fetch';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { useAppStore } from '@/store/app-store';
|
import { useAppStore } from '@/store/app-store';
|
||||||
|
|
||||||
@@ -24,18 +25,21 @@ interface InitScriptResponse {
|
|||||||
export function WorktreesSection({ useWorktrees, onUseWorktreesChange }: WorktreesSectionProps) {
|
export function WorktreesSection({ useWorktrees, onUseWorktreesChange }: WorktreesSectionProps) {
|
||||||
const currentProject = useAppStore((s) => s.currentProject);
|
const currentProject = useAppStore((s) => s.currentProject);
|
||||||
const [scriptContent, setScriptContent] = useState('');
|
const [scriptContent, setScriptContent] = useState('');
|
||||||
const [scriptPath, setScriptPath] = useState('');
|
const [originalContent, setOriginalContent] = useState('');
|
||||||
|
const [scriptExists, setScriptExists] = useState(false);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
const [showSaved, setShowSaved] = useState(false);
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
const saveTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
||||||
const savedTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
// Check if there are unsaved changes
|
||||||
|
const hasChanges = scriptContent !== originalContent;
|
||||||
|
|
||||||
// Load init script content when project changes
|
// Load init script content when project changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!currentProject?.path) {
|
if (!currentProject?.path) {
|
||||||
setScriptContent('');
|
setScriptContent('');
|
||||||
setScriptPath('');
|
setOriginalContent('');
|
||||||
|
setScriptExists(false);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -47,8 +51,10 @@ export function WorktreesSection({ useWorktrees, onUseWorktreesChange }: Worktre
|
|||||||
projectPath: currentProject.path,
|
projectPath: currentProject.path,
|
||||||
});
|
});
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
setScriptContent(response.content || '');
|
const content = response.content || '';
|
||||||
setScriptPath(response.path || '');
|
setScriptContent(content);
|
||||||
|
setOriginalContent(content);
|
||||||
|
setScriptExists(response.exists);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load init script:', error);
|
console.error('Failed to load init script:', error);
|
||||||
@@ -60,66 +66,74 @@ export function WorktreesSection({ useWorktrees, onUseWorktreesChange }: Worktre
|
|||||||
loadInitScript();
|
loadInitScript();
|
||||||
}, [currentProject?.path]);
|
}, [currentProject?.path]);
|
||||||
|
|
||||||
// Debounced save function
|
// Save script
|
||||||
const saveScript = useCallback(
|
const handleSave = useCallback(async () => {
|
||||||
async (content: string) => {
|
if (!currentProject?.path) return;
|
||||||
if (!currentProject?.path) return;
|
|
||||||
|
|
||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
try {
|
try {
|
||||||
const response = await apiPut<{ success: boolean; error?: string }>(
|
const response = await apiPut<{ success: boolean; error?: string }>(
|
||||||
'/api/worktree/init-script',
|
'/api/worktree/init-script',
|
||||||
{
|
{
|
||||||
projectPath: currentProject.path,
|
projectPath: currentProject.path,
|
||||||
content,
|
content: scriptContent,
|
||||||
}
|
|
||||||
);
|
|
||||||
if (response.success) {
|
|
||||||
setShowSaved(true);
|
|
||||||
savedTimeoutRef.current = setTimeout(() => setShowSaved(false), 2000);
|
|
||||||
} else {
|
|
||||||
toast.error('Failed to save init script', {
|
|
||||||
description: response.error,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
);
|
||||||
console.error('Failed to save init script:', error);
|
if (response.success) {
|
||||||
toast.error('Failed to save init script');
|
setOriginalContent(scriptContent);
|
||||||
} finally {
|
setScriptExists(true);
|
||||||
setIsSaving(false);
|
toast.success('Init script saved');
|
||||||
|
} else {
|
||||||
|
toast.error('Failed to save init script', {
|
||||||
|
description: response.error,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
} catch (error) {
|
||||||
[currentProject?.path]
|
console.error('Failed to save init script:', error);
|
||||||
);
|
toast.error('Failed to save init script');
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false);
|
||||||
|
}
|
||||||
|
}, [currentProject?.path, scriptContent]);
|
||||||
|
|
||||||
// Handle content change with debounce
|
// Reset to original content
|
||||||
const handleContentChange = useCallback(
|
const handleReset = useCallback(() => {
|
||||||
(value: string) => {
|
setScriptContent(originalContent);
|
||||||
setScriptContent(value);
|
}, [originalContent]);
|
||||||
setShowSaved(false);
|
|
||||||
|
|
||||||
// Clear existing timeouts
|
// Delete script
|
||||||
if (saveTimeoutRef.current) {
|
const handleDelete = useCallback(async () => {
|
||||||
clearTimeout(saveTimeoutRef.current);
|
if (!currentProject?.path) return;
|
||||||
}
|
|
||||||
if (savedTimeoutRef.current) {
|
setIsDeleting(true);
|
||||||
clearTimeout(savedTimeoutRef.current);
|
try {
|
||||||
|
const response = await apiDelete<{ success: boolean; error?: string }>(
|
||||||
|
'/api/worktree/init-script',
|
||||||
|
{
|
||||||
|
body: { projectPath: currentProject.path },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (response.success) {
|
||||||
|
setScriptContent('');
|
||||||
|
setOriginalContent('');
|
||||||
|
setScriptExists(false);
|
||||||
|
toast.success('Init script deleted');
|
||||||
|
} else {
|
||||||
|
toast.error('Failed to delete init script', {
|
||||||
|
description: response.error,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to delete init script:', error);
|
||||||
|
toast.error('Failed to delete init script');
|
||||||
|
} finally {
|
||||||
|
setIsDeleting(false);
|
||||||
|
}
|
||||||
|
}, [currentProject?.path]);
|
||||||
|
|
||||||
// Debounce save
|
// Handle content change (no auto-save)
|
||||||
saveTimeoutRef.current = setTimeout(() => {
|
const handleContentChange = useCallback((value: string) => {
|
||||||
saveScript(value);
|
setScriptContent(value);
|
||||||
}, 1000);
|
|
||||||
},
|
|
||||||
[saveScript]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Cleanup timeouts
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current);
|
|
||||||
if (savedTimeoutRef.current) clearTimeout(savedTimeoutRef.current);
|
|
||||||
};
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -177,24 +191,10 @@ export function WorktreesSection({ useWorktrees, onUseWorktreesChange }: Worktre
|
|||||||
<Terminal className="w-4 h-4 text-brand-500" />
|
<Terminal className="w-4 h-4 text-brand-500" />
|
||||||
<Label className="text-foreground font-medium">Initialization Script</Label>
|
<Label className="text-foreground font-medium">Initialization Script</Label>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
|
||||||
{isSaving && (
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
<Loader2 className="w-3 h-3 animate-spin" />
|
|
||||||
Saving...
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{showSaved && !isSaving && (
|
|
||||||
<span className="flex items-center gap-1 text-green-500">
|
|
||||||
<Check className="w-3 h-3" />
|
|
||||||
Saved
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground/80 leading-relaxed">
|
<p className="text-xs text-muted-foreground/80 leading-relaxed">
|
||||||
Shell commands to run after a worktree is created. Runs once per worktree. Uses Git
|
Shell commands to run after a worktree is created. Runs once per worktree. Uses Git Bash
|
||||||
Bash on Windows for cross-platform compatibility.
|
on Windows for cross-platform compatibility.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{currentProject ? (
|
{currentProject ? (
|
||||||
@@ -203,6 +203,9 @@ export function WorktreesSection({ useWorktrees, onUseWorktreesChange }: Worktre
|
|||||||
<div className="flex items-center gap-2 text-xs text-muted-foreground/60">
|
<div className="flex items-center gap-2 text-xs text-muted-foreground/60">
|
||||||
<FileCode className="w-3.5 h-3.5" />
|
<FileCode className="w-3.5 h-3.5" />
|
||||||
<code className="font-mono">.automaker/worktree-init.sh</code>
|
<code className="font-mono">.automaker/worktree-init.sh</code>
|
||||||
|
{hasChanges && (
|
||||||
|
<span className="text-amber-500 font-medium">(unsaved changes)</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
@@ -210,10 +213,11 @@ export function WorktreesSection({ useWorktrees, onUseWorktreesChange }: Worktre
|
|||||||
<Loader2 className="w-5 h-5 animate-spin text-muted-foreground" />
|
<Loader2 className="w-5 h-5 animate-spin text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<ShellSyntaxEditor
|
<>
|
||||||
value={scriptContent}
|
<ShellSyntaxEditor
|
||||||
onChange={handleContentChange}
|
value={scriptContent}
|
||||||
placeholder={`# Example initialization commands
|
onChange={handleContentChange}
|
||||||
|
placeholder={`# Example initialization commands
|
||||||
npm install
|
npm install
|
||||||
|
|
||||||
# Or use pnpm
|
# Or use pnpm
|
||||||
@@ -221,9 +225,52 @@ npm install
|
|||||||
|
|
||||||
# Copy environment file
|
# Copy environment file
|
||||||
# cp .env.example .env`}
|
# cp .env.example .env`}
|
||||||
minHeight="200px"
|
minHeight="200px"
|
||||||
data-testid="init-script-editor"
|
maxHeight="500px"
|
||||||
/>
|
data-testid="init-script-editor"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Action buttons */}
|
||||||
|
<div className="flex items-center justify-end gap-2 pt-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleReset}
|
||||||
|
disabled={!hasChanges || isSaving || isDeleting}
|
||||||
|
className="gap-1.5"
|
||||||
|
>
|
||||||
|
<RotateCcw className="w-3.5 h-3.5" />
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleDelete}
|
||||||
|
disabled={!scriptExists || isSaving || isDeleting}
|
||||||
|
className="gap-1.5 text-destructive hover:text-destructive hover:bg-destructive/10"
|
||||||
|
>
|
||||||
|
{isDeleting ? (
|
||||||
|
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Trash2 className="w-3.5 h-3.5" />
|
||||||
|
)}
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={!hasChanges || isSaving || isDeleting}
|
||||||
|
className="gap-1.5"
|
||||||
|
>
|
||||||
|
{isSaving ? (
|
||||||
|
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Save className="w-3.5 h-3.5" />
|
||||||
|
)}
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
Reference in New Issue
Block a user