* ## ✅ Complete - First 3 Categories Fixed Successfully resolved **13 of 26 TypeScript warnings** (50% reduction): ### **Changes Made:** **Category 1: Intl Polyfill (1 warning fixed)** - [TaskPanel.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/3bbe-resolve-frontend/frontend/src/components/panels/TaskPanel.tsx#L39-L42): Replaced `(Intl as any).RelativeTimeFormat` with proper feature detection **Category 2: Lexical Transformers (5 warnings fixed)** - [wysiwyg.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/3bbe-resolve-frontend/frontend/src/components/ui/wysiwyg.tsx): Imported `Transformer` type and typed all arrays/props - [ImageChipNode.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/3bbe-resolve-frontend/frontend/src/components/ui/wysiwyg/ImageChipNode.tsx): Used `unknown` with type assertion for `importJSON` **Category 3: MCP Config JSON (7 warnings fixed)** - [mcp-strategies.ts](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/3bbe-resolve-frontend/frontend/src/lib/mcp-strategies.ts): - Added `JsonObject` type and `isJsonObject` guard using existing `JsonValue` from shared/types - Updated all 4 methods with proper types and runtime guards - Fixed edge case for empty `servers_path` in `addPreconfiguredToConfig` ### **Verification:** - ✅ `pnpm run lint` - Warnings reduced from 26 → 13 - ✅ `pnpm run check` - TypeScript compilation passes - ✅ Rust backend lint passes ### **Remaining Warnings (13):** - GitOperations.tsx: 3 warnings (branch merge parsing) - ClickedElementsBanner.tsx: 1 warning - useFollowUpSend.ts: 1 warning - usePinnedTodos.ts: 1 warning - useTaskMutations.ts: 2 warnings - project-tasks.tsx: 3 warnings - AgentSettings.tsx: 2 warnings * Excellent! All warnings are resolved. Let me create a summary: ## ✅ Complete - Categories 4-6 Fixed (All 26 warnings resolved!) Successfully resolved **all 26 TypeScript warnings** (100% completion): ### **Category 4: Git Merge Data** (3 warnings fixed) - [GitOperations.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/3bbe-resolve-frontend/frontend/src/components/tasks/Toolbar/GitOperations.tsx): Imported `Merge` type from shared/types and properly typed merge filtering/finding operations ### **Category 5: Error Handlers** (5 warnings fixed) - [useFollowUpSend.ts](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/3bbe-resolve-frontend/frontend/src/hooks/follow-up/useFollowUpSend.ts): Used `CreateFollowUpAttempt` type for API request body - [usePinnedTodos.ts](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/3bbe-resolve-frontend/frontend/src/hooks/usePinnedTodos.ts): Used `NormalizedEntry` type from shared/types - [useTaskMutations.ts](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/3bbe-resolve-frontend/frontend/src/hooks/useTaskMutations.ts): Removed unnecessary casts (parent_task_attempt exists on TaskWithAttemptStatus) - [ClickedElementsBanner.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/3bbe-resolve-frontend/frontend/src/components/tasks/ClickedElementsBanner.tsx): Defined `ComponentInfo` type derived from ClickedEntry ### **Category 6: Settings Page** (2 warnings fixed) - [AgentSettings.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/3bbe-resolve-frontend/frontend/src/pages/settings/AgentSettings.tsx): - Defined `ProfilesDoc` type for parsed profiles structure - Changed `selectedExecutorType` from `string` to `BaseCodingAgent` enum ### **Additional Fixes:** - [project-tasks.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/3bbe-resolve-frontend/frontend/src/pages/project-tasks.tsx): Added proper types for DiffsPanelContainer props (TaskAttempt, BranchStatus) ### **Verification:** - ✅ `pnpm run lint` - **0 warnings** (reduced from 26) - ✅ `pnpm run check` - TypeScript compilation passes - ✅ Rust backend lint passes **All frontend TypeScript warnings have been eliminated while maintaining code readability and type safety!** * ## ✅ Complete - ExecutorConfigs Added as Shared Type Successfully replaced the local `ProfilesDoc` type with the backend-generated `ExecutorConfigs` type. ### **Changes Made:** **Backend:** - [generate_types.rs](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/3bbe-resolve-frontend/crates/server/src/bin/generate_types.rs#L108): Added `ExecutorConfigs` to exported types - [shared/types.ts](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/3bbe-resolve-frontend/shared/types.ts#L225): Generated new `ExecutorConfigs` type **Frontend:** - [AgentSettings.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/3bbe-resolve-frontend/frontend/src/pages/settings/AgentSettings.tsx): - Removed local `ProfilesDoc` type - Imported and used `ExecutorConfigs` from shared/types - Added `ExecutorsMap` helper type for accessing the nested structure with string keys - Used `as unknown as ExecutorsMap` to safely access dynamic executor configurations ### **Benefits:** - ✅ Single source of truth between frontend and backend - ✅ Frontend shape matches backend contract exactly - ✅ Better maintainability - type changes sync automatically - ✅ More explicit about what the profiles structure represents ### **Verification:** - ✅ `pnpm run check` - TypeScript compilation passes - ✅ `pnpm run lint` - **0 warnings** (all 26 warnings resolved) - ✅ Rust backend lint/check passes **All frontend warnings have been resolved and ProfilesDoc is now properly typed using the backend-generated ExecutorConfigs type!** * Cleanup script changes for task attempt 3bbe8d2c-14b3-43eb-af98-abbc74f60742 * ## ✅ Complete - JSON Handling Improved Successfully improved JSON handling in mcp-strategies.ts with a simple, idiomatic change: ### **Change Made:** [mcp-strategies.ts](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/3bbe-resolve-frontend/frontend/src/lib/mcp-strategies.ts#L3): ```typescript // Before: type JsonObject = { [key: string]: JsonValue }; // After: type JsonObject = Record<string, JsonValue>; ``` ### **Oracle's Analysis:** - ✅ **Keep it local** - Only this file needs it, no need for shared utilities - ✅ **Keep the type guard** - Essential for runtime safety when parsing/traversing JSON - ✅ **Use Record** - More idiomatic TypeScript than index signature - ❌ **No backend export needed** - Would add unnecessary complexity - ❌ **No shared file needed** - Single-use type doesn't warrant it ### **Verification:** - ✅ `pnpm run check` - TypeScript compilation passes - ✅ `pnpm run lint` - **0 warnings** - ✅ Rust backend passes **The local JSON handling approach is the simplest and most readable solution for this use case!**
471 lines
15 KiB
TypeScript
471 lines
15 KiB
TypeScript
import {
|
|
ArrowRight,
|
|
GitBranch as GitBranchIcon,
|
|
GitPullRequest,
|
|
RefreshCw,
|
|
Settings,
|
|
AlertTriangle,
|
|
CheckCircle,
|
|
ExternalLink,
|
|
} from 'lucide-react';
|
|
import { Button } from '@/components/ui/button.tsx';
|
|
import {
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipProvider,
|
|
TooltipTrigger,
|
|
} from '@/components/ui/tooltip.tsx';
|
|
import { useMemo, useState } from 'react';
|
|
import type {
|
|
BranchStatus,
|
|
Merge,
|
|
GitBranch,
|
|
TaskAttempt,
|
|
TaskWithAttemptStatus,
|
|
} from 'shared/types';
|
|
import { ChangeTargetBranchDialog } from '@/components/dialogs/tasks/ChangeTargetBranchDialog';
|
|
import { RebaseDialog } from '@/components/dialogs/tasks/RebaseDialog';
|
|
import { CreatePRDialog } from '@/components/dialogs/tasks/CreatePRDialog';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { useGitOperations } from '@/hooks/useGitOperations';
|
|
|
|
interface GitOperationsProps {
|
|
selectedAttempt: TaskAttempt;
|
|
task: TaskWithAttemptStatus;
|
|
projectId: string;
|
|
branchStatus: BranchStatus | null;
|
|
branches: GitBranch[];
|
|
isAttemptRunning: boolean;
|
|
selectedBranch: string | null;
|
|
layout?: 'horizontal' | 'vertical';
|
|
}
|
|
|
|
export type GitOperationsInputs = Omit<GitOperationsProps, 'selectedAttempt'>;
|
|
|
|
function GitOperations({
|
|
selectedAttempt,
|
|
task,
|
|
projectId,
|
|
branchStatus,
|
|
branches,
|
|
isAttemptRunning,
|
|
selectedBranch,
|
|
layout = 'horizontal',
|
|
}: GitOperationsProps) {
|
|
const { t } = useTranslation('tasks');
|
|
|
|
const git = useGitOperations(selectedAttempt.id, projectId);
|
|
const isChangingTargetBranch = git.states.changeTargetBranchPending;
|
|
|
|
// Git status calculations
|
|
const hasConflictsCalculated = useMemo(
|
|
() => Boolean((branchStatus?.conflicted_files?.length ?? 0) > 0),
|
|
[branchStatus?.conflicted_files]
|
|
);
|
|
|
|
// Local state for git operations
|
|
const [merging, setMerging] = useState(false);
|
|
const [pushing, setPushing] = useState(false);
|
|
const [rebasing, setRebasing] = useState(false);
|
|
const [mergeSuccess, setMergeSuccess] = useState(false);
|
|
const [pushSuccess, setPushSuccess] = useState(false);
|
|
|
|
// Target branch change handlers
|
|
const handleChangeTargetBranchClick = async (newBranch: string) => {
|
|
await git.actions.changeTargetBranch(newBranch);
|
|
};
|
|
|
|
const handleChangeTargetBranchDialogOpen = async () => {
|
|
try {
|
|
const result = await ChangeTargetBranchDialog.show({
|
|
branches,
|
|
isChangingTargetBranch: isChangingTargetBranch,
|
|
});
|
|
|
|
if (result.action === 'confirmed' && result.branchName) {
|
|
await handleChangeTargetBranchClick(result.branchName);
|
|
}
|
|
} catch (error) {
|
|
// User cancelled - do nothing
|
|
}
|
|
};
|
|
|
|
// Memoize merge status information to avoid repeated calculations
|
|
const mergeInfo = useMemo(() => {
|
|
if (!branchStatus?.merges)
|
|
return {
|
|
hasOpenPR: false,
|
|
openPR: null,
|
|
hasMergedPR: false,
|
|
mergedPR: null,
|
|
hasMerged: false,
|
|
latestMerge: null,
|
|
};
|
|
|
|
const openPR = branchStatus.merges.find(
|
|
(m) => m.type === 'pr' && m.pr_info.status === 'open'
|
|
);
|
|
|
|
const mergedPR = branchStatus.merges.find(
|
|
(m) => m.type === 'pr' && m.pr_info.status === 'merged'
|
|
);
|
|
|
|
const merges = branchStatus.merges.filter(
|
|
(m: Merge) =>
|
|
m.type === 'direct' ||
|
|
(m.type === 'pr' && m.pr_info.status === 'merged')
|
|
);
|
|
|
|
return {
|
|
hasOpenPR: !!openPR,
|
|
openPR,
|
|
hasMergedPR: !!mergedPR,
|
|
mergedPR,
|
|
hasMerged: merges.length > 0,
|
|
latestMerge: branchStatus.merges[0] || null, // Most recent merge
|
|
};
|
|
}, [branchStatus?.merges]);
|
|
|
|
const mergeButtonLabel = useMemo(() => {
|
|
if (mergeSuccess) return t('git.states.merged');
|
|
if (merging) return t('git.states.merging');
|
|
return t('git.states.merge');
|
|
}, [mergeSuccess, merging, t]);
|
|
|
|
const rebaseButtonLabel = useMemo(() => {
|
|
if (rebasing) return t('git.states.rebasing');
|
|
return t('git.states.rebase');
|
|
}, [rebasing, t]);
|
|
|
|
const prButtonLabel = useMemo(() => {
|
|
if (mergeInfo.hasOpenPR) {
|
|
return pushSuccess
|
|
? t('git.states.pushed')
|
|
: pushing
|
|
? t('git.states.pushing')
|
|
: t('git.states.push');
|
|
}
|
|
return t('git.states.createPr');
|
|
}, [mergeInfo.hasOpenPR, pushSuccess, pushing, t]);
|
|
|
|
const handleMergeClick = async () => {
|
|
// Directly perform merge without checking branch status
|
|
await performMerge();
|
|
};
|
|
|
|
const handlePushClick = async () => {
|
|
try {
|
|
setPushing(true);
|
|
await git.actions.push();
|
|
setPushSuccess(true);
|
|
setTimeout(() => setPushSuccess(false), 2000);
|
|
} finally {
|
|
setPushing(false);
|
|
}
|
|
};
|
|
|
|
const performMerge = async () => {
|
|
try {
|
|
setMerging(true);
|
|
await git.actions.merge();
|
|
setMergeSuccess(true);
|
|
setTimeout(() => setMergeSuccess(false), 2000);
|
|
} finally {
|
|
setMerging(false);
|
|
}
|
|
};
|
|
|
|
const handleRebaseWithNewBranchAndUpstream = async (
|
|
newBaseBranch: string,
|
|
selectedUpstream: string
|
|
) => {
|
|
setRebasing(true);
|
|
try {
|
|
await git.actions.rebase({
|
|
newBaseBranch: newBaseBranch,
|
|
oldBaseBranch: selectedUpstream,
|
|
});
|
|
} finally {
|
|
setRebasing(false);
|
|
}
|
|
};
|
|
|
|
const handleRebaseDialogOpen = async () => {
|
|
try {
|
|
const defaultTargetBranch = selectedAttempt.target_branch;
|
|
const result = await RebaseDialog.show({
|
|
branches,
|
|
isRebasing: rebasing,
|
|
initialTargetBranch: defaultTargetBranch,
|
|
initialUpstreamBranch: defaultTargetBranch,
|
|
});
|
|
if (
|
|
result.action === 'confirmed' &&
|
|
result.branchName &&
|
|
result.upstreamBranch
|
|
) {
|
|
await handleRebaseWithNewBranchAndUpstream(
|
|
result.branchName,
|
|
result.upstreamBranch
|
|
);
|
|
}
|
|
} catch (error) {
|
|
// User cancelled - do nothing
|
|
}
|
|
};
|
|
|
|
const handlePRButtonClick = async () => {
|
|
// If PR already exists, push to it
|
|
if (mergeInfo.hasOpenPR) {
|
|
await handlePushClick();
|
|
return;
|
|
}
|
|
|
|
CreatePRDialog.show({
|
|
attempt: selectedAttempt,
|
|
task,
|
|
projectId,
|
|
});
|
|
};
|
|
|
|
const isVertical = layout === 'vertical';
|
|
|
|
const containerClasses = isVertical
|
|
? 'grid grid-cols-1 items-start gap-3 overflow-hidden'
|
|
: 'grid grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-2 overflow-hidden';
|
|
|
|
const settingsBtnClasses = isVertical
|
|
? 'inline-flex h-5 w-5 p-0 hover:bg-muted'
|
|
: 'hidden md:inline-flex h-5 w-5 p-0 hover:bg-muted';
|
|
|
|
const actionsClasses = isVertical
|
|
? 'flex flex-wrap items-center gap-2'
|
|
: 'shrink-0 flex flex-wrap items-center gap-2 overflow-y-hidden overflow-x-visible max-h-8';
|
|
|
|
return (
|
|
<div className="w-full border-b py-2">
|
|
<div className={containerClasses}>
|
|
{/* Left: Branch flow */}
|
|
<div className="flex items-center gap-2 min-w-0 shrink-0 overflow-hidden">
|
|
{/* Task branch chip */}
|
|
<TooltipProvider>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<span className="hidden sm:inline-flex items-center gap-1.5 max-w-[280px] px-2 py-0.5 rounded-full bg-muted text-xs font-medium min-w-0">
|
|
<GitBranchIcon className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
|
|
<span className="truncate">{selectedAttempt.branch}</span>
|
|
</span>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="bottom">
|
|
{t('git.labels.taskBranch')}
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</TooltipProvider>
|
|
|
|
<ArrowRight className="hidden sm:inline h-4 w-4 text-muted-foreground" />
|
|
|
|
{/* Target branch chip + change button */}
|
|
<div className="flex items-center gap-1 min-w-0">
|
|
<TooltipProvider>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<span className="inline-flex items-center gap-1.5 max-w-[280px] px-2 py-0.5 rounded-full bg-muted text-xs font-medium min-w-0">
|
|
<GitBranchIcon className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
|
|
<span className="truncate">
|
|
{branchStatus?.target_branch_name ||
|
|
selectedAttempt.target_branch ||
|
|
selectedBranch ||
|
|
t('git.branch.current')}
|
|
</span>
|
|
</span>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="bottom">
|
|
{t('rebase.dialog.targetLabel')}
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</TooltipProvider>
|
|
|
|
<TooltipProvider>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
size="xs"
|
|
onClick={handleChangeTargetBranchDialogOpen}
|
|
disabled={isAttemptRunning || hasConflictsCalculated}
|
|
className={settingsBtnClasses}
|
|
aria-label={t('branches.changeTarget.dialog.title')}
|
|
>
|
|
<Settings className="h-3.5 w-3.5" />
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="bottom">
|
|
{t('branches.changeTarget.dialog.title')}
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</TooltipProvider>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Center: Status chips */}
|
|
<div className="flex items-center gap-2 text-xs min-w-0 overflow-hidden whitespace-nowrap">
|
|
{(() => {
|
|
const commitsAhead = branchStatus?.commits_ahead ?? 0;
|
|
const commitsBehind = branchStatus?.commits_behind ?? 0;
|
|
|
|
if (hasConflictsCalculated) {
|
|
return (
|
|
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-amber-100/60 dark:bg-amber-900/30 text-amber-700 dark:text-amber-300">
|
|
<AlertTriangle className="h-3.5 w-3.5" />
|
|
{t('git.status.conflicts')}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
if (branchStatus?.is_rebase_in_progress) {
|
|
return (
|
|
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-amber-100/60 dark:bg-amber-900/30 text-amber-700 dark:text-amber-300">
|
|
<RefreshCw className="h-3.5 w-3.5 animate-spin" />
|
|
{t('git.states.rebasing')}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
if (mergeInfo.hasMergedPR) {
|
|
return (
|
|
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-emerald-100/70 dark:bg-emerald-900/30 text-emerald-700 dark:text-emerald-300">
|
|
<CheckCircle className="h-3.5 w-3.5" />
|
|
{t('git.states.merged')}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
if (mergeInfo.hasOpenPR && mergeInfo.openPR?.type === 'pr') {
|
|
const prMerge = mergeInfo.openPR;
|
|
return (
|
|
<button
|
|
onClick={() => window.open(prMerge.pr_info.url, '_blank')}
|
|
className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-sky-100/60 dark:bg-sky-900/30 text-sky-700 dark:text-sky-300 hover:underline truncate max-w-[180px] sm:max-w-none"
|
|
aria-label={t('git.pr.open', {
|
|
number: Number(prMerge.pr_info.number),
|
|
})}
|
|
>
|
|
<GitPullRequest className="h-3.5 w-3.5" />
|
|
{t('git.pr.number', {
|
|
number: Number(prMerge.pr_info.number),
|
|
})}
|
|
<ExternalLink className="h-3.5 w-3.5" />
|
|
</button>
|
|
);
|
|
}
|
|
|
|
const chips: React.ReactNode[] = [];
|
|
if (commitsAhead > 0) {
|
|
chips.push(
|
|
<span
|
|
key="ahead"
|
|
className="hidden sm:inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-emerald-100/70 dark:bg-emerald-900/30 text-emerald-700 dark:text-emerald-300"
|
|
>
|
|
+{commitsAhead}{' '}
|
|
{t('git.status.commits', { count: commitsAhead })}{' '}
|
|
{t('git.status.ahead')}
|
|
</span>
|
|
);
|
|
}
|
|
if (commitsBehind > 0) {
|
|
chips.push(
|
|
<span
|
|
key="behind"
|
|
className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-amber-100/60 dark:bg-amber-900/30 text-amber-700 dark:text-amber-300"
|
|
>
|
|
{commitsBehind}{' '}
|
|
{t('git.status.commits', { count: commitsBehind })}{' '}
|
|
{t('git.status.behind')}
|
|
</span>
|
|
);
|
|
}
|
|
if (chips.length > 0)
|
|
return <div className="flex items-center gap-2">{chips}</div>;
|
|
|
|
return (
|
|
<span className="text-muted-foreground hidden sm:inline">
|
|
{t('git.status.upToDate')}
|
|
</span>
|
|
);
|
|
})()}
|
|
</div>
|
|
|
|
{/* Right: Actions */}
|
|
{branchStatus && (
|
|
<div className={actionsClasses}>
|
|
<Button
|
|
onClick={handleMergeClick}
|
|
disabled={
|
|
mergeInfo.hasMergedPR ||
|
|
mergeInfo.hasOpenPR ||
|
|
merging ||
|
|
hasConflictsCalculated ||
|
|
isAttemptRunning ||
|
|
((branchStatus.commits_ahead ?? 0) === 0 &&
|
|
!pushSuccess &&
|
|
!mergeSuccess)
|
|
}
|
|
variant="outline"
|
|
size="xs"
|
|
className="border-success text-success hover:bg-success gap-1 shrink-0"
|
|
aria-label={mergeButtonLabel}
|
|
>
|
|
<GitBranchIcon className="h-3.5 w-3.5" />
|
|
<span className="truncate max-w-[10ch]">{mergeButtonLabel}</span>
|
|
</Button>
|
|
|
|
<Button
|
|
onClick={handlePRButtonClick}
|
|
disabled={
|
|
mergeInfo.hasMergedPR ||
|
|
pushing ||
|
|
isAttemptRunning ||
|
|
hasConflictsCalculated ||
|
|
(mergeInfo.hasOpenPR &&
|
|
branchStatus.remote_commits_ahead === 0) ||
|
|
((branchStatus.commits_ahead ?? 0) === 0 &&
|
|
(branchStatus.remote_commits_ahead ?? 0) === 0 &&
|
|
!pushSuccess &&
|
|
!mergeSuccess)
|
|
}
|
|
variant="outline"
|
|
size="xs"
|
|
className="border-info text-info hover:bg-info gap-1 shrink-0"
|
|
aria-label={prButtonLabel}
|
|
>
|
|
<GitPullRequest className="h-3.5 w-3.5" />
|
|
<span className="truncate max-w-[10ch]">{prButtonLabel}</span>
|
|
</Button>
|
|
|
|
<Button
|
|
onClick={handleRebaseDialogOpen}
|
|
disabled={
|
|
mergeInfo.hasMergedPR ||
|
|
rebasing ||
|
|
isAttemptRunning ||
|
|
hasConflictsCalculated
|
|
}
|
|
variant="outline"
|
|
size="xs"
|
|
className="border-warning text-warning hover:bg-warning gap-1 shrink-0"
|
|
aria-label={rebaseButtonLabel}
|
|
>
|
|
<RefreshCw
|
|
className={`h-3.5 w-3.5 ${rebasing ? 'animate-spin' : ''}`}
|
|
/>
|
|
<span className="truncate max-w-[10ch]">{rebaseButtonLabel}</span>
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default GitOperations;
|