feat: add rename branch functionality with UI integration
This commit is contained in:
@@ -21,6 +21,7 @@ import {
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { BranchSelector } from './BranchSelector';
|
||||
import { WorktreeBranchDisplay } from './WorktreeBranchDisplay';
|
||||
import { SyncActions } from './SyncActions';
|
||||
import type { GitStatus, GitIdentityProfile } from '@/lib/api/types';
|
||||
|
||||
@@ -37,6 +38,7 @@ interface GitHeaderProps {
|
||||
onPush: () => void;
|
||||
onCheckoutBranch: (branch: string) => void;
|
||||
onCreateBranch: (name: string) => Promise<void>;
|
||||
onRenameBranch?: (oldName: string, newName: string) => Promise<void>;
|
||||
activeIdentityProfile: GitIdentityProfile | null;
|
||||
availableIdentities: GitIdentityProfile[];
|
||||
onSelectIdentity: (profile: GitIdentityProfile) => void;
|
||||
@@ -187,6 +189,7 @@ export const GitHeader: React.FC<GitHeaderProps> = ({
|
||||
onPush,
|
||||
onCheckoutBranch,
|
||||
onCreateBranch,
|
||||
onRenameBranch,
|
||||
activeIdentityProfile,
|
||||
availableIdentities,
|
||||
onSelectIdentity,
|
||||
@@ -199,7 +202,12 @@ export const GitHeader: React.FC<GitHeaderProps> = ({
|
||||
|
||||
return (
|
||||
<header className="flex flex-wrap items-center gap-2 border-b border-border/40 px-3 py-2 bg-background">
|
||||
{!isWorktreeMode && (
|
||||
{isWorktreeMode ? (
|
||||
<WorktreeBranchDisplay
|
||||
currentBranch={status.current}
|
||||
onRename={onRenameBranch}
|
||||
/>
|
||||
) : (
|
||||
<BranchSelector
|
||||
currentBranch={status.current}
|
||||
localBranches={localBranches}
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
import React from 'react';
|
||||
import { RiGitBranchLine, RiEditLine, RiCheckLine, RiCloseLine, RiLoader4Line } from '@remixicon/react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
interface WorktreeBranchDisplayProps {
|
||||
currentBranch: string | null | undefined;
|
||||
onRename?: (oldName: string, newName: string) => Promise<void>;
|
||||
}
|
||||
|
||||
const sanitizeBranchNameInput = (value: string): string => {
|
||||
return value
|
||||
.trim()
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/[^A-Za-z0-9._/-]/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/\/{2,}/g, '/')
|
||||
.replace(/\/-+/g, '/')
|
||||
.replace(/-+\//g, '/')
|
||||
.replace(/^[-/]+/, '')
|
||||
.replace(/[-/]+$/, '');
|
||||
};
|
||||
|
||||
export const WorktreeBranchDisplay: React.FC<WorktreeBranchDisplayProps> = ({
|
||||
currentBranch,
|
||||
onRename,
|
||||
}) => {
|
||||
const [isEditing, setIsEditing] = React.useState(false);
|
||||
const [editBranchName, setEditBranchName] = React.useState(currentBranch || '');
|
||||
const [isRenaming, setIsRenaming] = React.useState(false);
|
||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleStartEdit = () => {
|
||||
if (!currentBranch || !onRename) return;
|
||||
setEditBranchName(currentBranch);
|
||||
setIsEditing(true);
|
||||
// Focus input after state update
|
||||
setTimeout(() => inputRef.current?.focus(), 0);
|
||||
};
|
||||
|
||||
const handleSaveEdit = async () => {
|
||||
if (!currentBranch || !onRename || !editBranchName.trim()) return;
|
||||
|
||||
const sanitizedName = sanitizeBranchNameInput(editBranchName);
|
||||
if (sanitizedName === currentBranch) {
|
||||
setIsEditing(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsRenaming(true);
|
||||
try {
|
||||
await onRename(currentBranch, sanitizedName);
|
||||
setIsEditing(false);
|
||||
setEditBranchName('');
|
||||
} finally {
|
||||
setIsRenaming(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancelEdit = () => {
|
||||
setIsEditing(false);
|
||||
setEditBranchName('');
|
||||
};
|
||||
|
||||
// Handle Enter key to save, Escape to cancel
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
handleSaveEdit();
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
handleCancelEdit();
|
||||
}
|
||||
};
|
||||
|
||||
if (isEditing) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 rounded-md bg-primary/12 px-2 py-1 h-8">
|
||||
<form
|
||||
className="flex w-full items-center gap-2"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleSaveEdit();
|
||||
}}
|
||||
>
|
||||
<RiGitBranchLine className="size-4 text-primary" />
|
||||
<input
|
||||
ref={inputRef}
|
||||
value={editBranchName}
|
||||
onChange={(e) => setEditBranchName(e.target.value)}
|
||||
className="flex-1 min-w-0 bg-transparent typography-ui-label outline-none placeholder:text-muted-foreground"
|
||||
placeholder="Branch name"
|
||||
onKeyDown={handleKeyDown}
|
||||
autoFocus
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isRenaming}
|
||||
className="shrink-0 text-muted-foreground hover:text-foreground disabled:opacity-50"
|
||||
>
|
||||
{isRenaming ? (
|
||||
<RiLoader4Line className="size-4 animate-spin" />
|
||||
) : (
|
||||
<RiCheckLine className="size-4" />
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCancelEdit}
|
||||
disabled={isRenaming}
|
||||
className="shrink-0 text-muted-foreground hover:text-foreground disabled:opacity-50"
|
||||
>
|
||||
<RiCloseLine className="size-4" />
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-1.5 px-2 py-1 h-8">
|
||||
<RiGitBranchLine className="size-4 text-primary" />
|
||||
<span className="max-w-[140px] truncate typography-ui-label font-normal text-foreground">
|
||||
{currentBranch || 'Detached HEAD'}
|
||||
</span>
|
||||
</div>
|
||||
{onRename && currentBranch && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={handleStartEdit}
|
||||
title="Rename branch"
|
||||
>
|
||||
<RiEditLine className="size-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user