mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-03-18 22:33:08 +00:00
refactor: Improve all git operations, add stash support, add improved pull request flow, add worktree file copy options, address code review comments, add cherry pick options
This commit is contained in:
@@ -10,6 +10,7 @@ import { ProjectModelsSection } from './project-models-section';
|
||||
import { DataManagementSection } from './data-management-section';
|
||||
import { DangerZoneSection } from '../settings-view/danger-zone/danger-zone-section';
|
||||
import { DeleteProjectDialog } from '../settings-view/components/delete-project-dialog';
|
||||
import { RemoveFromAutomakerDialog } from '../settings-view/components/remove-from-automaker-dialog';
|
||||
import { ProjectSettingsNavigation } from './components/project-settings-navigation';
|
||||
import { useProjectSettingsView } from './hooks/use-project-settings-view';
|
||||
import type { Project as ElectronProject } from '@/lib/electron';
|
||||
@@ -28,8 +29,9 @@ interface SettingsProject {
|
||||
}
|
||||
|
||||
export function ProjectSettingsView() {
|
||||
const { currentProject, moveProjectToTrash } = useAppStore();
|
||||
const { currentProject, moveProjectToTrash, removeProject } = useAppStore();
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||
const [showRemoveFromAutomakerDialog, setShowRemoveFromAutomakerDialog] = useState(false);
|
||||
|
||||
// Use project settings view navigation hook
|
||||
const { activeView, navigateTo } = useProjectSettingsView();
|
||||
@@ -98,6 +100,7 @@ export function ProjectSettingsView() {
|
||||
<DangerZoneSection
|
||||
project={settingsProject}
|
||||
onDeleteClick={() => setShowDeleteDialog(true)}
|
||||
onRemoveFromAutomakerClick={() => setShowRemoveFromAutomakerDialog(true)}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
@@ -178,6 +181,14 @@ export function ProjectSettingsView() {
|
||||
project={currentProject}
|
||||
onConfirm={moveProjectToTrash}
|
||||
/>
|
||||
|
||||
{/* Remove from Automaker Confirmation Dialog */}
|
||||
<RemoveFromAutomakerDialog
|
||||
open={showRemoveFromAutomakerDialog}
|
||||
onOpenChange={setShowRemoveFromAutomakerDialog}
|
||||
project={currentProject}
|
||||
onConfirm={removeProject}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useState, useEffect, useCallback } from 'react';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { ShellSyntaxEditor } from '@/components/ui/shell-syntax-editor';
|
||||
import {
|
||||
GitBranch,
|
||||
@@ -11,6 +12,9 @@ import {
|
||||
RotateCcw,
|
||||
Trash2,
|
||||
PanelBottomClose,
|
||||
Copy,
|
||||
Plus,
|
||||
FolderOpen,
|
||||
} from 'lucide-react';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
import { cn } from '@/lib/utils';
|
||||
@@ -19,6 +23,7 @@ import { toast } from 'sonner';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import { getHttpApiClient } from '@/lib/http-api-client';
|
||||
import type { Project } from '@/lib/electron';
|
||||
import { ProjectFileSelectorDialog } from '@/components/dialogs/project-file-selector-dialog';
|
||||
|
||||
interface WorktreePreferencesSectionProps {
|
||||
project: Project;
|
||||
@@ -42,6 +47,8 @@ export function WorktreePreferencesSection({ project }: WorktreePreferencesSecti
|
||||
const setDefaultDeleteBranch = useAppStore((s) => s.setDefaultDeleteBranch);
|
||||
const getAutoDismissInitScriptIndicator = useAppStore((s) => s.getAutoDismissInitScriptIndicator);
|
||||
const setAutoDismissInitScriptIndicator = useAppStore((s) => s.setAutoDismissInitScriptIndicator);
|
||||
const getWorktreeCopyFiles = useAppStore((s) => s.getWorktreeCopyFiles);
|
||||
const setWorktreeCopyFiles = useAppStore((s) => s.setWorktreeCopyFiles);
|
||||
|
||||
// Get effective worktrees setting (project override or global fallback)
|
||||
const projectUseWorktrees = getProjectUseWorktrees(project.path);
|
||||
@@ -54,6 +61,11 @@ export function WorktreePreferencesSection({ project }: WorktreePreferencesSecti
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
|
||||
// Copy files state
|
||||
const [newCopyFilePath, setNewCopyFilePath] = useState('');
|
||||
const [fileSelectorOpen, setFileSelectorOpen] = useState(false);
|
||||
const copyFiles = getWorktreeCopyFiles(project.path);
|
||||
|
||||
// Get the current settings for this project
|
||||
const showIndicator = getShowInitScriptIndicator(project.path);
|
||||
const defaultDeleteBranch = getDefaultDeleteBranch(project.path);
|
||||
@@ -93,6 +105,9 @@ export function WorktreePreferencesSection({ project }: WorktreePreferencesSecti
|
||||
response.settings.autoDismissInitScriptIndicator
|
||||
);
|
||||
}
|
||||
if (response.settings.worktreeCopyFiles !== undefined) {
|
||||
setWorktreeCopyFiles(currentPath, response.settings.worktreeCopyFiles);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (!isCancelled) {
|
||||
@@ -112,6 +127,7 @@ export function WorktreePreferencesSection({ project }: WorktreePreferencesSecti
|
||||
setShowInitScriptIndicator,
|
||||
setDefaultDeleteBranch,
|
||||
setAutoDismissInitScriptIndicator,
|
||||
setWorktreeCopyFiles,
|
||||
]);
|
||||
|
||||
// Load init script content when project changes
|
||||
@@ -219,6 +235,97 @@ export function WorktreePreferencesSection({ project }: WorktreePreferencesSecti
|
||||
setScriptContent(value);
|
||||
}, []);
|
||||
|
||||
// Add a new file path to copy list
|
||||
const handleAddCopyFile = useCallback(async () => {
|
||||
const trimmed = newCopyFilePath.trim();
|
||||
if (!trimmed) return;
|
||||
|
||||
// Normalize: remove leading ./ or /
|
||||
const normalized = trimmed.replace(/^\.\//, '').replace(/^\//, '');
|
||||
if (!normalized) return;
|
||||
|
||||
// Check for duplicates
|
||||
const currentFiles = getWorktreeCopyFiles(project.path);
|
||||
if (currentFiles.includes(normalized)) {
|
||||
toast.error('File already in list', {
|
||||
description: `"${normalized}" is already configured for copying.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedFiles = [...currentFiles, normalized];
|
||||
setWorktreeCopyFiles(project.path, updatedFiles);
|
||||
setNewCopyFilePath('');
|
||||
|
||||
// Persist to server
|
||||
try {
|
||||
const httpClient = getHttpApiClient();
|
||||
await httpClient.settings.updateProject(project.path, {
|
||||
worktreeCopyFiles: updatedFiles,
|
||||
});
|
||||
toast.success('Copy file added', {
|
||||
description: `"${normalized}" will be copied to new worktrees.`,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to persist worktreeCopyFiles:', error);
|
||||
toast.error('Failed to save copy files setting');
|
||||
}
|
||||
}, [project.path, newCopyFilePath, getWorktreeCopyFiles, setWorktreeCopyFiles]);
|
||||
|
||||
// Remove a file path from copy list
|
||||
const handleRemoveCopyFile = useCallback(
|
||||
async (filePath: string) => {
|
||||
const currentFiles = getWorktreeCopyFiles(project.path);
|
||||
const updatedFiles = currentFiles.filter((f) => f !== filePath);
|
||||
setWorktreeCopyFiles(project.path, updatedFiles);
|
||||
|
||||
// Persist to server
|
||||
try {
|
||||
const httpClient = getHttpApiClient();
|
||||
await httpClient.settings.updateProject(project.path, {
|
||||
worktreeCopyFiles: updatedFiles,
|
||||
});
|
||||
toast.success('Copy file removed');
|
||||
} catch (error) {
|
||||
console.error('Failed to persist worktreeCopyFiles:', error);
|
||||
toast.error('Failed to save copy files setting');
|
||||
}
|
||||
},
|
||||
[project.path, getWorktreeCopyFiles, setWorktreeCopyFiles]
|
||||
);
|
||||
|
||||
// Handle files selected from the file selector dialog
|
||||
const handleFileSelectorSelect = useCallback(
|
||||
async (paths: string[]) => {
|
||||
const currentFiles = getWorktreeCopyFiles(project.path);
|
||||
|
||||
// Filter out duplicates
|
||||
const newPaths = paths.filter((p) => !currentFiles.includes(p));
|
||||
if (newPaths.length === 0) {
|
||||
toast.info('All selected files are already in the list');
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedFiles = [...currentFiles, ...newPaths];
|
||||
setWorktreeCopyFiles(project.path, updatedFiles);
|
||||
|
||||
// Persist to server
|
||||
try {
|
||||
const httpClient = getHttpApiClient();
|
||||
await httpClient.settings.updateProject(project.path, {
|
||||
worktreeCopyFiles: updatedFiles,
|
||||
});
|
||||
toast.success(`${newPaths.length} ${newPaths.length === 1 ? 'file' : 'files'} added`, {
|
||||
description: newPaths.map((p) => `"${p}"`).join(', '),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to persist worktreeCopyFiles:', error);
|
||||
toast.error('Failed to save copy files setting');
|
||||
}
|
||||
},
|
||||
[project.path, getWorktreeCopyFiles, setWorktreeCopyFiles]
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -387,6 +494,92 @@ export function WorktreePreferencesSection({ project }: WorktreePreferencesSecti
|
||||
{/* Separator */}
|
||||
<div className="border-t border-border/30" />
|
||||
|
||||
{/* Copy Files Section */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Copy className="w-4 h-4 text-brand-500" />
|
||||
<Label className="text-foreground font-medium">Copy Files to Worktrees</Label>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground/80 leading-relaxed">
|
||||
Specify files or directories (relative to project root) to automatically copy into new
|
||||
worktrees. Useful for untracked files like{' '}
|
||||
<code className="font-mono text-foreground/60">.env</code>,{' '}
|
||||
<code className="font-mono text-foreground/60">.env.local</code>, or local config files
|
||||
that aren't committed to git.
|
||||
</p>
|
||||
|
||||
{/* Current file list */}
|
||||
{copyFiles.length > 0 && (
|
||||
<div className="space-y-1.5">
|
||||
{copyFiles.map((filePath) => (
|
||||
<div
|
||||
key={filePath}
|
||||
className="flex items-center gap-2 group/item px-3 py-1.5 rounded-lg bg-accent/20 hover:bg-accent/40 transition-colors"
|
||||
>
|
||||
<FileCode className="w-3.5 h-3.5 text-muted-foreground/60 flex-shrink-0" />
|
||||
<code className="font-mono text-sm text-foreground/80 flex-1 truncate">
|
||||
{filePath}
|
||||
</code>
|
||||
<button
|
||||
onClick={() => handleRemoveCopyFile(filePath)}
|
||||
className="p-0.5 rounded text-muted-foreground/50 hover:bg-destructive/10 hover:text-destructive transition-all flex-shrink-0"
|
||||
title={`Remove ${filePath}`}
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Add new file input */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
value={newCopyFilePath}
|
||||
onChange={(e) => setNewCopyFilePath(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
handleAddCopyFile();
|
||||
}
|
||||
}}
|
||||
placeholder=".env, config/local.json, etc."
|
||||
className="flex-1 h-8 text-sm font-mono"
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleAddCopyFile}
|
||||
disabled={!newCopyFilePath.trim()}
|
||||
className="gap-1.5 h-8"
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5" />
|
||||
Add
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setFileSelectorOpen(true)}
|
||||
className="gap-1.5 h-8"
|
||||
>
|
||||
<FolderOpen className="w-3.5 h-3.5" />
|
||||
Browse
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* File selector dialog */}
|
||||
<ProjectFileSelectorDialog
|
||||
open={fileSelectorOpen}
|
||||
onOpenChange={setFileSelectorOpen}
|
||||
onSelect={handleFileSelectorSelect}
|
||||
projectPath={project.path}
|
||||
existingFiles={copyFiles}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Separator */}
|
||||
<div className="border-t border-border/30" />
|
||||
|
||||
{/* Init Script Section */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
|
||||
Reference in New Issue
Block a user