mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-03-24 00:13:07 +00:00
277 lines
8.7 KiB
TypeScript
277 lines
8.7 KiB
TypeScript
import { useState, useEffect, useCallback } from 'react';
|
|
import { createLogger } from '@automaker/utils/logger';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from '@/components/ui/dialog';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Label } from '@/components/ui/label';
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@/components/ui/select';
|
|
import { getHttpApiClient } from '@/lib/http-api-client';
|
|
import { getErrorMessage } from '@/lib/utils';
|
|
import { Download, Upload, RefreshCw, AlertTriangle } from 'lucide-react';
|
|
import { Spinner } from '@/components/ui/spinner';
|
|
import type { WorktreeInfo } from '../worktree-panel/types';
|
|
|
|
interface RemoteInfo {
|
|
name: string;
|
|
url: string;
|
|
}
|
|
|
|
const logger = createLogger('SelectRemoteDialog');
|
|
|
|
export type SelectRemoteOperation = 'pull' | 'push';
|
|
|
|
interface SelectRemoteDialogProps {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
worktree: WorktreeInfo | null;
|
|
operation: SelectRemoteOperation;
|
|
onConfirm: (worktree: WorktreeInfo, remote: string) => void;
|
|
}
|
|
|
|
export function SelectRemoteDialog({
|
|
open,
|
|
onOpenChange,
|
|
worktree,
|
|
operation,
|
|
onConfirm,
|
|
}: SelectRemoteDialogProps) {
|
|
const [remotes, setRemotes] = useState<RemoteInfo[]>([]);
|
|
const [selectedRemote, setSelectedRemote] = useState<string>('');
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const fetchRemotes = useCallback(async () => {
|
|
if (!worktree) return;
|
|
|
|
setIsLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const api = getHttpApiClient();
|
|
const result = await api.worktree.listRemotes(worktree.path);
|
|
|
|
if (result.success && result.result) {
|
|
const remoteInfos = result.result.remotes.map((r: { name: string; url: string }) => ({
|
|
name: r.name,
|
|
url: r.url,
|
|
}));
|
|
setRemotes(remoteInfos);
|
|
setSelectedRemote((prev) => {
|
|
if (prev && remoteInfos.some((r) => r.name === prev)) {
|
|
return prev;
|
|
}
|
|
return remoteInfos.find((r) => r.name === 'origin')?.name ?? remoteInfos[0]?.name ?? '';
|
|
});
|
|
} else {
|
|
setError(result.error || 'Failed to fetch remotes');
|
|
}
|
|
} catch (err) {
|
|
logger.error('Failed to fetch remotes:', err);
|
|
setError(getErrorMessage(err));
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}, [worktree]);
|
|
|
|
// Fetch remotes when dialog opens
|
|
useEffect(() => {
|
|
if (open && worktree) {
|
|
fetchRemotes();
|
|
}
|
|
}, [open, worktree, fetchRemotes]);
|
|
|
|
// Reset state when dialog closes
|
|
useEffect(() => {
|
|
if (!open) {
|
|
setSelectedRemote('');
|
|
setError(null);
|
|
}
|
|
}, [open]);
|
|
|
|
// Auto-select default remote when remotes are loaded
|
|
useEffect(() => {
|
|
if (remotes.length > 0 && !selectedRemote) {
|
|
// Default to 'origin' if available, otherwise first remote
|
|
const defaultRemote = remotes.find((r) => r.name === 'origin') || remotes[0];
|
|
setSelectedRemote(defaultRemote.name);
|
|
}
|
|
}, [remotes, selectedRemote]);
|
|
|
|
const handleRefresh = async () => {
|
|
if (!worktree) return;
|
|
|
|
setIsRefreshing(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const api = getHttpApiClient();
|
|
const result = await api.worktree.listRemotes(worktree.path);
|
|
|
|
if (result.success && result.result) {
|
|
const remoteInfos = result.result.remotes.map((r: { name: string; url: string }) => ({
|
|
name: r.name,
|
|
url: r.url,
|
|
}));
|
|
setRemotes(remoteInfos);
|
|
setSelectedRemote((prev) => {
|
|
if (prev && remoteInfos.some((r) => r.name === prev)) {
|
|
return prev;
|
|
}
|
|
return remoteInfos.find((r) => r.name === 'origin')?.name ?? remoteInfos[0]?.name ?? '';
|
|
});
|
|
} else {
|
|
setError(result.error || 'Failed to refresh remotes');
|
|
}
|
|
} catch (err) {
|
|
logger.error('Failed to refresh remotes:', err);
|
|
setError(getErrorMessage(err));
|
|
} finally {
|
|
setIsRefreshing(false);
|
|
}
|
|
};
|
|
|
|
const handleConfirm = () => {
|
|
if (!worktree || !selectedRemote) return;
|
|
onConfirm(worktree, selectedRemote);
|
|
onOpenChange(false);
|
|
};
|
|
|
|
const isPull = operation === 'pull';
|
|
const Icon = isPull ? Download : Upload;
|
|
const title = isPull ? 'Pull from Remote' : 'Push to Remote';
|
|
const actionLabel = isPull
|
|
? `Pull from ${selectedRemote || 'Remote'}`
|
|
: `Push to ${selectedRemote || 'Remote'}`;
|
|
const description = isPull ? (
|
|
<>
|
|
Select a remote to pull changes into{' '}
|
|
<span className="font-mono text-foreground">{worktree?.branch || 'current branch'}</span>
|
|
</>
|
|
) : (
|
|
<>
|
|
Select a remote to push{' '}
|
|
<span className="font-mono text-foreground">{worktree?.branch || 'current branch'}</span> to
|
|
</>
|
|
);
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="sm:max-w-[450px]">
|
|
<DialogHeader>
|
|
<DialogTitle className="flex items-center gap-2">
|
|
<Icon className="w-5 h-5 text-primary" />
|
|
{title}
|
|
</DialogTitle>
|
|
<DialogDescription>{description}</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
{isLoading ? (
|
|
<div className="flex items-center justify-center py-8">
|
|
<Spinner size="lg" />
|
|
</div>
|
|
) : error ? (
|
|
<div className="flex flex-col items-center gap-4 py-6">
|
|
<div className="flex items-center gap-2 text-destructive">
|
|
<AlertTriangle className="w-5 h-5" />
|
|
<span className="text-sm">{error}</span>
|
|
</div>
|
|
<Button variant="outline" size="sm" onClick={fetchRemotes}>
|
|
<RefreshCw className="w-4 h-4 mr-2" />
|
|
Retry
|
|
</Button>
|
|
</div>
|
|
) : (
|
|
<div className="grid gap-4 py-4">
|
|
<div className="grid gap-2">
|
|
<div className="flex items-center justify-between">
|
|
<Label htmlFor="remote-select">Select Remote</Label>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={handleRefresh}
|
|
disabled={isRefreshing}
|
|
className="h-6 px-2 text-xs"
|
|
>
|
|
{isRefreshing ? (
|
|
<Spinner size="xs" className="mr-1" />
|
|
) : (
|
|
<RefreshCw className="w-3 h-3 mr-1" />
|
|
)}
|
|
Refresh
|
|
</Button>
|
|
</div>
|
|
<Select value={selectedRemote} onValueChange={setSelectedRemote}>
|
|
<SelectTrigger id="remote-select">
|
|
<SelectValue placeholder="Select a remote" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{remotes.map((remote) => (
|
|
<SelectItem
|
|
key={remote.name}
|
|
value={remote.name}
|
|
description={
|
|
<span className="text-xs text-muted-foreground truncate max-w-[300px]">
|
|
{remote.url}
|
|
</span>
|
|
}
|
|
>
|
|
<span className="font-medium">{remote.name}</span>
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{selectedRemote && (
|
|
<div className="mt-2 p-3 rounded-md bg-muted/50 border border-border">
|
|
<p className="text-sm text-muted-foreground">
|
|
{isPull ? (
|
|
<>
|
|
This will pull changes from{' '}
|
|
<span className="font-mono text-foreground">
|
|
{selectedRemote}/{worktree?.branch}
|
|
</span>{' '}
|
|
into your local branch.
|
|
</>
|
|
) : (
|
|
<>
|
|
This will push your local changes to{' '}
|
|
<span className="font-mono text-foreground">
|
|
{selectedRemote}/{worktree?.branch}
|
|
</span>
|
|
.
|
|
</>
|
|
)}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
|
Cancel
|
|
</Button>
|
|
<Button onClick={handleConfirm} disabled={!selectedRemote || isLoading}>
|
|
<Icon className="w-4 h-4 mr-2" />
|
|
{actionLabel}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|