299 lines
8.5 KiB
TypeScript
299 lines
8.5 KiB
TypeScript
import React from 'react';
|
|
import {
|
|
RiArrowDownSLine,
|
|
RiCheckLine,
|
|
RiLoader4Line,
|
|
RiGitBranchLine,
|
|
RiBriefcaseLine,
|
|
RiHomeLine,
|
|
RiGraduationCapLine,
|
|
RiCodeLine,
|
|
RiHeartLine,
|
|
RiHistoryLine,
|
|
RiUser3Line,
|
|
} from '@remixicon/react';
|
|
import { Button } from '@/components/ui/button';
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger,
|
|
} 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, GitRemote } from '@/lib/api/types';
|
|
import { useI18n } from '@/lib/i18n';
|
|
|
|
type SyncAction = 'fetch' | 'pull' | 'push' | null;
|
|
|
|
interface GitHeaderProps {
|
|
status: GitStatus | null;
|
|
localBranches: string[];
|
|
remoteBranches: string[];
|
|
branchInfo: Record<string, { ahead?: number; behind?: number }> | undefined;
|
|
syncAction: SyncAction;
|
|
remotes: GitRemote[];
|
|
onFetch: (remote: GitRemote) => void;
|
|
onPull: (remote: GitRemote) => void;
|
|
onPush: () => void;
|
|
onRemoveRemote: (remote: GitRemote) => void;
|
|
removingRemoteName: string | null;
|
|
onCheckoutBranch: (branch: string) => void;
|
|
onCreateBranch: (name: string, remote?: GitRemote) => Promise<void>;
|
|
onRenameBranch?: (oldName: string, newName: string) => Promise<void>;
|
|
activeIdentityProfile: GitIdentityProfile | null;
|
|
availableIdentities: GitIdentityProfile[];
|
|
onSelectIdentity: (profile: GitIdentityProfile) => void;
|
|
isApplyingIdentity: boolean;
|
|
isWorktreeMode: boolean;
|
|
onOpenHistory?: () => void;
|
|
}
|
|
|
|
const IDENTITY_ICON_MAP: Record<
|
|
string,
|
|
React.ComponentType<React.ComponentProps<typeof RiGitBranchLine>>
|
|
> = {
|
|
branch: RiGitBranchLine,
|
|
briefcase: RiBriefcaseLine,
|
|
house: RiHomeLine,
|
|
graduation: RiGraduationCapLine,
|
|
code: RiCodeLine,
|
|
heart: RiHeartLine,
|
|
user: RiUser3Line,
|
|
};
|
|
|
|
const IDENTITY_COLOR_MAP: Record<string, string> = {
|
|
keyword: 'var(--syntax-keyword)',
|
|
error: 'var(--status-error)',
|
|
string: 'var(--syntax-string)',
|
|
function: 'var(--syntax-function)',
|
|
type: 'var(--syntax-type)',
|
|
success: 'var(--status-success)',
|
|
info: 'var(--status-info)',
|
|
warning: 'var(--status-warning)',
|
|
};
|
|
|
|
function getIdentityColor(token?: string | null) {
|
|
if (!token) {
|
|
return 'var(--primary)';
|
|
}
|
|
return IDENTITY_COLOR_MAP[token] || 'var(--primary)';
|
|
}
|
|
|
|
interface IdentityIconProps {
|
|
icon?: string | null;
|
|
className?: string;
|
|
colorToken?: string | null;
|
|
}
|
|
|
|
const IdentityIcon: React.FC<IdentityIconProps> = ({ icon, className, colorToken }) => {
|
|
const IconComponent = IDENTITY_ICON_MAP[icon ?? 'branch'] ?? RiUser3Line;
|
|
return (
|
|
<IconComponent
|
|
className={className}
|
|
style={{ color: getIdentityColor(colorToken) }}
|
|
/>
|
|
);
|
|
};
|
|
|
|
interface IdentityDropdownProps {
|
|
activeProfile: GitIdentityProfile | null;
|
|
identities: GitIdentityProfile[];
|
|
onSelect: (profile: GitIdentityProfile) => void;
|
|
isApplying: boolean;
|
|
iconOnly?: boolean;
|
|
}
|
|
|
|
const IdentityDropdown: React.FC<IdentityDropdownProps> = ({
|
|
activeProfile,
|
|
identities,
|
|
onSelect,
|
|
isApplying,
|
|
iconOnly = false,
|
|
}) => {
|
|
const { t } = useI18n();
|
|
const isDisabled = isApplying || identities.length === 0;
|
|
|
|
return (
|
|
<DropdownMenu>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-8 min-w-0 max-w-[15rem] justify-start gap-1.5 px-2 py-1 typography-ui-label"
|
|
style={{ color: getIdentityColor(activeProfile?.color) }}
|
|
disabled={isDisabled}
|
|
>
|
|
{isApplying ? (
|
|
<RiLoader4Line className="size-4 animate-spin" />
|
|
) : (
|
|
<IdentityIcon
|
|
icon={activeProfile?.icon}
|
|
colorToken={activeProfile?.color}
|
|
className="size-4"
|
|
/>
|
|
)}
|
|
{!iconOnly && (
|
|
<span className="git-identity-label min-w-0 flex-1 truncate text-left">
|
|
{activeProfile?.name || t('gitView.header.noIdentity')}
|
|
</span>
|
|
)}
|
|
<RiArrowDownSLine className="size-4 opacity-60" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
</TooltipTrigger>
|
|
<TooltipContent sideOffset={8}>{t('gitView.header.identityTooltip')}</TooltipContent>
|
|
</Tooltip>
|
|
<DropdownMenuContent align="end" className="w-64">
|
|
{identities.length === 0 ? (
|
|
<div className="px-2 py-1.5">
|
|
<p className="typography-meta text-muted-foreground">
|
|
{t('gitView.header.noProfiles')}
|
|
</p>
|
|
</div>
|
|
) : (
|
|
identities.map((profile) => {
|
|
const isSelected = activeProfile?.id === profile.id;
|
|
return (
|
|
<DropdownMenuItem key={profile.id} onSelect={() => onSelect(profile)}>
|
|
<span className="flex items-center gap-2">
|
|
<IdentityIcon
|
|
icon={profile.icon}
|
|
colorToken={profile.color}
|
|
className="size-4"
|
|
/>
|
|
<span className="flex min-w-0 flex-col">
|
|
<span className="typography-ui-label text-foreground">
|
|
{profile.name}
|
|
</span>
|
|
<span className="typography-meta text-muted-foreground">
|
|
{profile.userEmail}
|
|
</span>
|
|
</span>
|
|
{isSelected ? (
|
|
<RiCheckLine className="ml-auto size-4 text-foreground" />
|
|
) : null}
|
|
</span>
|
|
</DropdownMenuItem>
|
|
);
|
|
})
|
|
)}
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
);
|
|
};
|
|
|
|
export const GitHeader: React.FC<GitHeaderProps> = ({
|
|
status,
|
|
localBranches,
|
|
remoteBranches,
|
|
branchInfo,
|
|
syncAction,
|
|
remotes,
|
|
onFetch,
|
|
onPull,
|
|
onPush,
|
|
onRemoveRemote,
|
|
removingRemoteName,
|
|
onCheckoutBranch,
|
|
onCreateBranch,
|
|
onRenameBranch,
|
|
activeIdentityProfile,
|
|
availableIdentities,
|
|
onSelectIdentity,
|
|
isApplyingIdentity,
|
|
isWorktreeMode,
|
|
onOpenHistory,
|
|
}) => {
|
|
const { t } = useI18n();
|
|
if (!status) {
|
|
return null;
|
|
}
|
|
|
|
const managementButtons = (
|
|
<div className="flex items-center gap-1 shrink-0">
|
|
{onOpenHistory ? (
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-8 w-8 px-0"
|
|
onClick={onOpenHistory}
|
|
>
|
|
<RiHistoryLine className="size-4" />
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent sideOffset={8}>{t('gitView.history.title')}</TooltipContent>
|
|
</Tooltip>
|
|
) : null}
|
|
</div>
|
|
);
|
|
|
|
const syncButtons = (
|
|
<SyncActions
|
|
syncAction={syncAction}
|
|
remotes={remotes}
|
|
onFetch={onFetch}
|
|
onPull={onPull}
|
|
onPush={onPush}
|
|
onRemoveRemote={onRemoveRemote}
|
|
removingRemoteName={removingRemoteName}
|
|
disabled={!status}
|
|
iconOnly={true}
|
|
|
|
aheadCount={status.ahead}
|
|
behindCount={status.behind}
|
|
/>
|
|
);
|
|
|
|
const identityControl = (
|
|
<IdentityDropdown
|
|
activeProfile={activeIdentityProfile}
|
|
identities={availableIdentities}
|
|
onSelect={onSelectIdentity}
|
|
isApplying={isApplyingIdentity}
|
|
|
|
iconOnly={false}
|
|
/>
|
|
);
|
|
|
|
return (
|
|
<header className="@container/git-header px-3 py-2 bg-transparent">
|
|
<div className="flex items-center justify-between gap-2 min-w-0">
|
|
<div className="min-w-0 flex-1">
|
|
{isWorktreeMode ? (
|
|
<WorktreeBranchDisplay
|
|
currentBranch={status.current}
|
|
onRename={onRenameBranch}
|
|
/>
|
|
) : (
|
|
<BranchSelector
|
|
currentBranch={status.current}
|
|
localBranches={localBranches}
|
|
remoteBranches={remoteBranches}
|
|
branchInfo={branchInfo}
|
|
onCheckout={onCheckoutBranch}
|
|
onCreate={onCreateBranch}
|
|
remotes={remotes}
|
|
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-1.5 flex items-center justify-between gap-2 min-w-0">
|
|
<div className="flex min-w-0 flex-wrap items-center gap-1">
|
|
{syncButtons}
|
|
{managementButtons}
|
|
</div>
|
|
<div className="min-w-0 max-w-[45%]">{identityControl}</div>
|
|
</div>
|
|
</header>
|
|
);
|
|
};
|