Add Push button to Git panel when PR is open with unpushed commits (Vibe Kanban) (#1951)
* The implementation is complete. Here's a summary of what I changed:
## Summary
Added a "Push" button to the workspaces UI Git panel. The changes allow users to push changes when a PR is already open.
### Files Modified:
1. **`frontend/src/components/ui-new/primitives/RepoCard.tsx`** (lines 27-42)
- Added `'push'` to the `RepoAction` type union
- Added a new option to `repoActionOptions` array with the Push button configuration using `ArrowUpIcon`
2. **`frontend/src/components/ui-new/actions/index.ts`** (lines 33, 691-707)
- Imported `ArrowUpIcon` from phosphor-icons
- Added `GitPush` action definition that:
- Calls `attemptsApi.push()` with the workspace and repo IDs
- Handles the `force_push_required` error case
- Invalidates workspace queries on success to refresh the UI
3. **`frontend/src/components/ui-new/containers/WorkspacesLayout.tsx`** (line 103)
- Added mapping from `'push'` action to `Actions.GitPush` in the action handler
The Push button now appears in the Git panel dropdown alongside "Open pull request" and "Merge" options.
* The type check passes. Here's a summary of the updated changes:
## Summary
The Push button now appears **next to the PR number** when:
1. A PR is already open (`prStatus === 'open'`)
2. There are new commits ahead (`commitsAhead > 0`)
### Changes Made:
**`frontend/src/components/ui-new/primitives/RepoCard.tsx`**:
- Removed `push` from the `repoActionOptions` dropdown (lines 34-41)
- Added a dedicated Push button in the PR status row (lines 213-222) that:
- Only appears when `prStatus === 'open'` AND `commitsAhead > 0`
- Uses brand primary styling (`bg-brand-primary`) to make it prominent
- Displays with an up arrow icon and "Push" label using existing translation
- Calls `onActionsClick?.('push')` when clicked
The `GitPush` action and handler mapping from the previous changes remain in place to handle the push operation.
* Done. The Push button now uses the same styling as the "Open PR" button: `bg-panel text-normal hover:bg-tertiary`.
* The type check passes. Here's a summary of the fix:
## Summary
The Push button now correctly uses `remoteCommitsAhead` (unpushed commits to remote) instead of `commitsAhead` (total commits ahead of target branch) to determine when to show.
### Changes Made:
1. **`frontend/src/components/ui-new/views/GitPanel.tsx`**:
- Added `remoteCommitsAhead: number` to the `RepoInfo` interface
- Pass `remoteCommitsAhead` to `RepoCard`
2. **`frontend/src/components/ui-new/primitives/RepoCard.tsx`**:
- Added `remoteCommitsAhead?: number` to `RepoCardProps`
- Changed the push button condition from `commitsAhead > 0` to `remoteCommitsAhead > 0`
3. **`frontend/src/components/ui-new/containers/WorkspacesLayout.tsx`**:
- Added `remoteCommitsAhead: repoStatus?.remote_commits_ahead ?? 0` to the `repoInfos` mapping
Now the Push button will:
- Only appear when there's an open PR AND there are unpushed commits (`remote_commits_ahead > 0`)
- Disappear after a successful push (when `remote_commits_ahead` becomes 0)
- Reappear when new commits are made locally that haven't been pushed
* The type check passes. Here's a summary of the changes:
## Summary
The Push button now hides immediately when clicked and only reappears when new commits are available:
### Changes to `RepoCard.tsx`:
- Added `useState` and `useRef` imports
- Added `isPushHidden` state to track when the button should be hidden
- Added `lastRemoteCommitsAhead` ref to track the previous value
- Added `useEffect` that resets `isPushHidden` to `false` when `remoteCommitsAhead` increases (meaning new commits are available)
- Added `handlePushClick` function that hides the button immediately and triggers the push action
- Updated the button condition to include `!isPushHidden`
The button will:
1. Hide immediately when clicked (no double-clicking possible)
2. Stay hidden until `remoteCommitsAhead` increases above the last known value
3. Reappear when new local commits are made that need to be pushed
* Both the linter and type check pass. The linter errors are fixed. Here's a summary of the changes:
## Summary of Linter Fixes
The linter was flagging that `RepoCard` (a presentational component) should not manage state or have side effects. I refactored the code to lift the state up to the container component:
### Changes:
1. **`RepoCard.tsx`** - Made purely presentational:
- Removed `useState`, `useRef`, `useEffect` imports
- Removed `remoteCommitsAhead` prop
- Added `showPushButton` prop (boolean, controlled by parent)
- Added `onPushClick` callback prop
- The button now just renders based on `showPushButton` and calls `onPushClick`
2. **`GitPanel.tsx`** - Updated interface and props:
- `RepoInfo` now has optional `remoteCommitsAhead` and `showPushButton` fields
- Added `onPushClick` callback to `GitPanelProps`
- Passes `showPushButton` and `onPushClick` to `RepoCard`
3. **`WorkspacesLayout.tsx`** - State management moved here:
- Added `hiddenPushRepos` state (Set of repo IDs where push was clicked)
- Added `lastRemoteCommitsAhead` ref to track previous values
- Added `useEffect` to reset hidden state when `remoteCommitsAhead` increases
- Added `repoInfosWithPushButton` memo that computes `showPushButton` for each repo
- Added `handlePushClick` that hides button immediately and executes push action
* prettier fix
* Done. I've added `GitPush` to the Git actions list in the command bar. It now appears between "Merge" and "Rebase" in the Git Actions menu.
* Both type check and lint pass.
Done! The Push action in the command bar now has the same visibility conditions as in the Git panel - it only appears when:
1. There's a workspace
2. There are git repos
3. There's an open PR (`hasOpenPR`)
4. There are unpushed commits (`hasUnpushedCommits`)
### Summary of changes:
1. **`frontend/src/components/ui-new/actions/index.ts`**:
- Added `hasOpenPR` and `hasUnpushedCommits` to `ActionVisibilityContext`
- Updated `GitPush.isVisible` to check these new context properties
2. **`frontend/src/components/ui-new/actions/useActionVisibility.ts`**:
- Added `useBranchStatus` hook to get branch status data
- Computed `hasOpenPR` by checking if any repo has an open PR in merges
- Computed `hasUnpushedCommits` by checking if any repo has `remote_commits_ahead > 0`
- Added these values to the returned context
* The refactoring is complete. Here's a summary of the changes:
## Summary of Changes
The push button logic has been simplified by using React Query's mutation state instead of manually tracking hidden state with refs and effects.
### Key Changes:
1. **WorkspacesLayout.tsx (`GitPanelContainer`)**:
- Removed the complex `useEffect` that tracked `lastRemoteCommitsAhead` and managed `hiddenPushRepos` state
- Removed `hiddenPushRepos` state and `lastRemoteCommitsAhead` ref
- Added `usePush` hook with callbacks that:
- On success: awaits query invalidation before clearing the loading state
- On error: clears the loading state
- Added simple `pushingRepoId` state to track which repo is being pushed
- The button now shows loading state during push and automatically disappears after refetch if `remoteCommitsAhead` becomes 0
2. **GitPanel.tsx**:
- Added `isPushPending?: boolean` to `RepoInfo` interface
- Passes `isPushPending` to `RepoCard`
3. **RepoCard.tsx**:
- Added `isPushPending` prop
- Button shows while either `showPushButton` or `isPushPending` is true
- Button is disabled and shows spinner with "Pushing..." text when pending
- Uses existing translation key `git.states.pushing`
### How it works now:
1. User clicks push button
2. Button immediately shows loading state (spinner + "Pushing...")
3. Button is disabled to prevent multiple clicks
4. Push mutation runs
5. On success, query invalidation completes before clearing loading state
6. Once `branchStatus` refetches, if `remoteCommitsAhead` is 0, `showPushButton` becomes false and button disappears
7. On error, loading state clears and button returns to normal
This approach is simpler and more predictable than manually tracking previous values.
* Done. The push button now shows success feedback like the old UI:
## Summary of Changes
The push button now has three states:
1. **Normal**: Shows "Push" with an up arrow icon
2. **Pending**: Shows "Pushing..." with a spinning icon, button is disabled
3. **Success**: Shows "Pushed!" with a green checkmark icon and green background for 2 seconds
The flow is:
1. User clicks "Push"
2. Button immediately shows "Pushing..." with spinner
3. Push completes, queries are invalidated and refetched
4. Button shows green "Pushed!" for 2 seconds
5. Button disappears (if no more commits to push) or returns to normal state
* All code review issues have been addressed:
## Summary of Changes
1. **Unified push logic** - Now uses `executeAction(Actions.GitPush, ...)` instead of calling `usePush` hook directly. This ensures consistent error handling between the button and command bar.
2. **Proper setTimeout cleanup** - Added `successTimeoutRef` with a cleanup effect that clears the timeout when the component unmounts:
```typescript
const successTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
useEffect(() => {
return () => {
if (successTimeoutRef.current) {
clearTimeout(successTimeoutRef.current);
}
};
}, []);
```
3. **Error feedback** - Added error state handling:
- New `isPushError` prop flows through GitPanel to RepoCard
- Button shows red "Failed" with warning icon when push fails
- Error state clears after 3 seconds
- Added `git.states.pushFailed` translation key
4. **Simplified state management** - Changed from separate `pushingRepoId` and `pushSuccessRepoId` to a single `pushStates` record with `PushState` type (`'idle' | 'pending' | 'success' | 'error'`)
* All i18n checks pass now.
* Both code review issues have been addressed:
## Summary of Changes
### 1. Fixed stale closure issue
- Added a `pushStatesRef` that mirrors the `pushStates` state
- Changed the check from `pushStates[repoId]` to `pushStatesRef.current[repoId]`
- Removed `pushStates` from the dependency array
### 2. Added proper error logging and user-facing error message
- Modified `ActionsContext.tsx` to re-throw errors after showing the dialog, so callers can handle them
- Added `console.error('Push failed:', error)` for debugging purposes
- The error dialog is already shown by `executeAction` via `ConfirmDialog`
- The button now properly shows the error state when push fails
* fmt
* The fix is in place. The key change is:
- When we're in a "push flow" (state is `pending`, `success`, or `error`), we set `showPushButton: false` but the button remains visible because of the `isPushPending || isPushSuccess || isPushError` condition in RepoCard
- This prevents the race condition where the query refetch could hide the button before we set the success state
* All checks pass. The changes now:
1. **Reverted ActionsContext** - Removed the re-throw to maintain backwards compatibility
2. **Use `usePush` hook directly** - For proper success/error detection instead of going through `executeAction`
3. **Show error dialog manually** - Using `ConfirmDialog.show()` when push fails
4. **Track current repo with ref** - `currentPushRepoRef` tracks which repo is being pushed so callbacks can update the correct state
* All checks pass. Here's a summary of the force push handling:
## Force Push Functionality
Yes, force push is fully supported. Here's how it works:
**Old UI (`useGitOperations.ts`):**
1. When `usePush` fails with `errorData.type === 'force_push_required'`
2. It automatically shows `ForcePushDialog`
3. The dialog warns the user about the consequences and lets them confirm
4. If confirmed, it calls `useForcePush` to execute the force push
**New UI (now updated):**
1. When the push button is clicked and fails with `force_push_required`
2. The push state is reset to `idle` (so the button returns to normal)
3. `ForcePushDialog` is shown with the attemptId and repoId
4. User can confirm or cancel the force push from the dialog
The `ForcePushDialog` handles its own loading state, success/error feedback, and API call internally via `useForcePush` hook.
* clear pushStates on workspace change
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -30,6 +30,7 @@ import {
|
||||
CrosshairIcon,
|
||||
DesktopIcon,
|
||||
PencilSimpleIcon,
|
||||
ArrowUpIcon,
|
||||
} from '@phosphor-icons/react';
|
||||
import { useDiffViewStore } from '@/stores/useDiffViewStore';
|
||||
import { useUiPreferencesStore } from '@/stores/useUiPreferencesStore';
|
||||
@@ -99,6 +100,8 @@ export interface ActionVisibilityContext {
|
||||
// Git panel state
|
||||
hasGitRepos: boolean;
|
||||
hasMultipleRepos: boolean;
|
||||
hasOpenPR: boolean;
|
||||
hasUnpushedCommits: boolean;
|
||||
}
|
||||
|
||||
// Base properties shared by all actions
|
||||
@@ -686,6 +689,30 @@ export const Actions = {
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
GitPush: {
|
||||
id: 'git-push',
|
||||
label: 'Push',
|
||||
icon: ArrowUpIcon,
|
||||
requiresTarget: 'git',
|
||||
isVisible: (ctx) =>
|
||||
ctx.hasWorkspace &&
|
||||
ctx.hasGitRepos &&
|
||||
ctx.hasOpenPR &&
|
||||
ctx.hasUnpushedCommits,
|
||||
execute: async (ctx, workspaceId, repoId) => {
|
||||
const result = await attemptsApi.push(workspaceId, { repo_id: repoId });
|
||||
if (!result.success) {
|
||||
if (result.error?.type === 'force_push_required') {
|
||||
throw new Error(
|
||||
'Force push required. The remote branch has diverged.'
|
||||
);
|
||||
}
|
||||
throw new Error('Failed to push changes');
|
||||
}
|
||||
invalidateWorkspaceQueries(ctx.queryClient, workspaceId);
|
||||
},
|
||||
},
|
||||
} as const satisfies Record<string, ActionDefinition>;
|
||||
|
||||
// Helper to resolve dynamic label
|
||||
|
||||
@@ -166,6 +166,7 @@ export const Pages: Record<StaticPageId, CommandBarPage> = {
|
||||
items: [
|
||||
{ type: 'action', action: Actions.GitCreatePR },
|
||||
{ type: 'action', action: Actions.GitMerge },
|
||||
{ type: 'action', action: Actions.GitPush },
|
||||
{ type: 'action', action: Actions.GitRebase },
|
||||
{ type: 'action', action: Actions.GitChangeTarget },
|
||||
],
|
||||
|
||||
@@ -5,7 +5,8 @@ import { useUiPreferencesStore } from '@/stores/useUiPreferencesStore';
|
||||
import { useWorkspaceContext } from '@/contexts/WorkspaceContext';
|
||||
import { useUserSystem } from '@/components/ConfigProvider';
|
||||
import { useDevServer } from '@/hooks/useDevServer';
|
||||
import type { Workspace } from 'shared/types';
|
||||
import { useBranchStatus } from '@/hooks/useBranchStatus';
|
||||
import type { Workspace, Merge } from 'shared/types';
|
||||
import type {
|
||||
ActionVisibilityContext,
|
||||
ActionDefinition,
|
||||
@@ -29,6 +30,7 @@ export function useActionVisibilityContext(): ActionVisibilityContext {
|
||||
const { config } = useUserSystem();
|
||||
const { isStarting, isStopping, runningDevServers } =
|
||||
useDevServer(workspaceId);
|
||||
const { data: branchStatus } = useBranchStatus(workspaceId);
|
||||
|
||||
return useMemo(() => {
|
||||
// Compute isAllDiffsExpanded
|
||||
@@ -45,6 +47,18 @@ export function useActionVisibilityContext(): ActionVisibilityContext {
|
||||
? 'running'
|
||||
: 'stopped';
|
||||
|
||||
// Compute git state from branch status
|
||||
const hasOpenPR =
|
||||
branchStatus?.some((repo) =>
|
||||
repo.merges?.some(
|
||||
(m: Merge) => m.type === 'pr' && m.pr_info.status === 'open'
|
||||
)
|
||||
) ?? false;
|
||||
|
||||
const hasUnpushedCommits =
|
||||
branchStatus?.some((repo) => (repo.remote_commits_ahead ?? 0) > 0) ??
|
||||
false;
|
||||
|
||||
return {
|
||||
isChangesMode: layout.isChangesMode,
|
||||
isLogsMode: layout.isLogsMode,
|
||||
@@ -63,6 +77,8 @@ export function useActionVisibilityContext(): ActionVisibilityContext {
|
||||
runningDevServers,
|
||||
hasGitRepos: repos.length > 0,
|
||||
hasMultipleRepos: repos.length > 1,
|
||||
hasOpenPR,
|
||||
hasUnpushedCommits,
|
||||
};
|
||||
}, [
|
||||
layout.isChangesMode,
|
||||
@@ -81,6 +97,7 @@ export function useActionVisibilityContext(): ActionVisibilityContext {
|
||||
isStarting,
|
||||
isStopping,
|
||||
runningDevServers,
|
||||
branchStatus,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,10 @@ import { NavbarContainer } from '@/components/ui-new/containers/NavbarContainer'
|
||||
import { PreviewBrowserContainer } from '@/components/ui-new/containers/PreviewBrowserContainer';
|
||||
import { PreviewControlsContainer } from '@/components/ui-new/containers/PreviewControlsContainer';
|
||||
import { useRenameBranch } from '@/hooks/useRenameBranch';
|
||||
import { usePush } from '@/hooks/usePush';
|
||||
import { repoApi } from '@/lib/api';
|
||||
import { ConfirmDialog } from '@/components/ui-new/dialogs/ConfirmDialog';
|
||||
import { ForcePushDialog } from '@/components/dialogs/git/ForcePushDialog';
|
||||
import { useDiffStream } from '@/hooks/useDiffStream';
|
||||
import { useTask } from '@/hooks/useTask';
|
||||
import { useAttemptRepo } from '@/hooks/useAttemptRepo';
|
||||
@@ -53,6 +56,8 @@ interface GitPanelContainerProps {
|
||||
onBranchNameChange: (name: string) => void;
|
||||
}
|
||||
|
||||
type PushState = 'idle' | 'pending' | 'success' | 'error';
|
||||
|
||||
function GitPanelContainer({
|
||||
selectedWorkspace,
|
||||
repos,
|
||||
@@ -61,6 +66,101 @@ function GitPanelContainer({
|
||||
}: GitPanelContainerProps) {
|
||||
const { executeAction } = useActions();
|
||||
|
||||
// Track push state per repo: idle, pending, success, or error
|
||||
const [pushStates, setPushStates] = useState<Record<string, PushState>>({});
|
||||
const pushStatesRef = useRef<Record<string, PushState>>({});
|
||||
pushStatesRef.current = pushStates;
|
||||
const successTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const currentPushRepoRef = useRef<string | null>(null);
|
||||
|
||||
// Reset push-related state when the selected workspace changes to avoid
|
||||
// leaking push state across workspaces with repos that share the same ID.
|
||||
useEffect(() => {
|
||||
setPushStates({});
|
||||
pushStatesRef.current = {};
|
||||
currentPushRepoRef.current = null;
|
||||
|
||||
if (successTimeoutRef.current) {
|
||||
clearTimeout(successTimeoutRef.current);
|
||||
successTimeoutRef.current = null;
|
||||
}
|
||||
}, [selectedWorkspace?.id]);
|
||||
// Use push hook for direct API access with proper error handling
|
||||
const pushMutation = usePush(
|
||||
selectedWorkspace?.id,
|
||||
// onSuccess
|
||||
() => {
|
||||
const repoId = currentPushRepoRef.current;
|
||||
if (!repoId) return;
|
||||
setPushStates((prev) => ({ ...prev, [repoId]: 'success' }));
|
||||
// Clear success state after 2 seconds
|
||||
successTimeoutRef.current = setTimeout(() => {
|
||||
setPushStates((prev) => ({ ...prev, [repoId]: 'idle' }));
|
||||
}, 2000);
|
||||
},
|
||||
// onError
|
||||
async (err, errorData) => {
|
||||
const repoId = currentPushRepoRef.current;
|
||||
if (!repoId) return;
|
||||
|
||||
// Handle force push required - show confirmation dialog
|
||||
if (errorData?.type === 'force_push_required' && selectedWorkspace?.id) {
|
||||
setPushStates((prev) => ({ ...prev, [repoId]: 'idle' }));
|
||||
await ForcePushDialog.show({
|
||||
attemptId: selectedWorkspace.id,
|
||||
repoId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Show error state and dialog for other errors
|
||||
setPushStates((prev) => ({ ...prev, [repoId]: 'error' }));
|
||||
const message =
|
||||
err instanceof Error ? err.message : 'Failed to push changes';
|
||||
ConfirmDialog.show({
|
||||
title: 'Error',
|
||||
message,
|
||||
confirmText: 'OK',
|
||||
showCancelButton: false,
|
||||
variant: 'destructive',
|
||||
});
|
||||
// Clear error state after 3 seconds
|
||||
successTimeoutRef.current = setTimeout(() => {
|
||||
setPushStates((prev) => ({ ...prev, [repoId]: 'idle' }));
|
||||
}, 3000);
|
||||
}
|
||||
);
|
||||
|
||||
// Clean up timeout on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (successTimeoutRef.current) {
|
||||
clearTimeout(successTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Compute repoInfos with push button state
|
||||
const repoInfosWithPushButton = useMemo(
|
||||
() =>
|
||||
repoInfos.map((repo) => {
|
||||
const state = pushStates[repo.id] ?? 'idle';
|
||||
const hasUnpushedCommits =
|
||||
repo.prStatus === 'open' && (repo.remoteCommitsAhead ?? 0) > 0;
|
||||
// Show push button if there are unpushed commits OR if we're in a push flow
|
||||
// (pending/success/error states keep the button visible for feedback)
|
||||
const isInPushFlow = state !== 'idle';
|
||||
return {
|
||||
...repo,
|
||||
showPushButton: hasUnpushedCommits && !isInPushFlow,
|
||||
isPushPending: state === 'pending',
|
||||
isPushSuccess: state === 'success',
|
||||
isPushError: state === 'error',
|
||||
};
|
||||
}),
|
||||
[repoInfos, pushStates]
|
||||
);
|
||||
|
||||
// Handle copying repo path to clipboard
|
||||
const handleCopyPath = useCallback(
|
||||
(repoId: string) => {
|
||||
@@ -100,6 +200,7 @@ function GitPanelContainer({
|
||||
merge: Actions.GitMerge,
|
||||
rebase: Actions.GitRebase,
|
||||
'change-target': Actions.GitChangeTarget,
|
||||
push: Actions.GitPush,
|
||||
};
|
||||
|
||||
const actionDef = actionMap[action];
|
||||
@@ -111,12 +212,33 @@ function GitPanelContainer({
|
||||
[selectedWorkspace, executeAction]
|
||||
);
|
||||
|
||||
// Handle push button click - use mutation for proper state tracking
|
||||
const handlePushClick = useCallback(
|
||||
(repoId: string) => {
|
||||
// Use ref to check current state to avoid stale closure
|
||||
if (pushStatesRef.current[repoId] === 'pending') return;
|
||||
|
||||
// Clear any existing timeout
|
||||
if (successTimeoutRef.current) {
|
||||
clearTimeout(successTimeoutRef.current);
|
||||
successTimeoutRef.current = null;
|
||||
}
|
||||
|
||||
// Track which repo we're pushing
|
||||
currentPushRepoRef.current = repoId;
|
||||
setPushStates((prev) => ({ ...prev, [repoId]: 'pending' }));
|
||||
pushMutation.mutate({ repo_id: repoId });
|
||||
},
|
||||
[pushMutation]
|
||||
);
|
||||
|
||||
return (
|
||||
<GitPanel
|
||||
repos={repoInfos}
|
||||
repos={repoInfosWithPushButton}
|
||||
workingBranchName={selectedWorkspace?.branch ?? ''}
|
||||
onWorkingBranchNameChange={onBranchNameChange}
|
||||
onActionsClick={handleActionsClick}
|
||||
onPushClick={handlePushClick}
|
||||
onOpenInEditor={handleOpenInEditor}
|
||||
onCopyPath={handleCopyPath}
|
||||
onAddRepo={() => console.log('Add repo clicked')}
|
||||
@@ -256,6 +378,7 @@ export function WorkspacesLayout() {
|
||||
name: repo.display_name || repo.name,
|
||||
targetBranch: repo.target_branch || 'main',
|
||||
commitsAhead: repoStatus?.commits_ahead ?? 0,
|
||||
remoteCommitsAhead: repoStatus?.remote_commits_ahead ?? 0,
|
||||
filesChanged: diffStats.filesChanged,
|
||||
linesAdded: diffStats.linesAdded,
|
||||
linesRemoved: diffStats.linesRemoved,
|
||||
|
||||
@@ -11,6 +11,8 @@ import {
|
||||
CopyIcon,
|
||||
GitMergeIcon,
|
||||
CheckCircleIcon,
|
||||
SpinnerGapIcon,
|
||||
WarningCircleIcon,
|
||||
} from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
@@ -24,7 +26,12 @@ import { CollapsibleSection } from './CollapsibleSection';
|
||||
import { SplitButton, type SplitButtonOption } from './SplitButton';
|
||||
import { useRepoAction, PERSIST_KEYS } from '@/stores/useUiPreferencesStore';
|
||||
|
||||
export type RepoAction = 'pull-request' | 'merge' | 'change-target' | 'rebase';
|
||||
export type RepoAction =
|
||||
| 'pull-request'
|
||||
| 'merge'
|
||||
| 'change-target'
|
||||
| 'rebase'
|
||||
| 'push';
|
||||
|
||||
const repoActionOptions: SplitButtonOption<RepoAction>[] = [
|
||||
{
|
||||
@@ -46,10 +53,15 @@ interface RepoCardProps {
|
||||
prNumber?: number;
|
||||
prUrl?: string;
|
||||
prStatus?: 'open' | 'merged' | 'closed' | 'unknown';
|
||||
showPushButton?: boolean;
|
||||
isPushPending?: boolean;
|
||||
isPushSuccess?: boolean;
|
||||
isPushError?: boolean;
|
||||
branchDropdownContent?: React.ReactNode;
|
||||
onChangeTarget?: () => void;
|
||||
onRebase?: () => void;
|
||||
onActionsClick?: (action: RepoAction) => void;
|
||||
onPushClick?: () => void;
|
||||
onOpenInEditor?: () => void;
|
||||
onCopyPath?: () => void;
|
||||
}
|
||||
@@ -65,10 +77,15 @@ export function RepoCard({
|
||||
prNumber,
|
||||
prUrl,
|
||||
prStatus,
|
||||
showPushButton = false,
|
||||
isPushPending = false,
|
||||
isPushSuccess = false,
|
||||
isPushError = false,
|
||||
branchDropdownContent,
|
||||
onChangeTarget,
|
||||
onRebase,
|
||||
onActionsClick,
|
||||
onPushClick,
|
||||
onOpenInEditor,
|
||||
onCopyPath,
|
||||
}: RepoCardProps) {
|
||||
@@ -205,6 +222,40 @@ export function RepoCard({
|
||||
{t('git.pr.open', { number: prNumber })}
|
||||
</span>
|
||||
)}
|
||||
{/* Push button - shows loading/success/error state */}
|
||||
{(showPushButton ||
|
||||
isPushPending ||
|
||||
isPushSuccess ||
|
||||
isPushError) && (
|
||||
<button
|
||||
onClick={onPushClick}
|
||||
disabled={isPushPending || isPushSuccess || isPushError}
|
||||
className={`inline-flex items-center gap-half px-base py-half rounded-sm text-sm font-medium transition-colors disabled:cursor-not-allowed ${
|
||||
isPushSuccess
|
||||
? 'bg-success/20 text-success'
|
||||
: isPushError
|
||||
? 'bg-error/20 text-error'
|
||||
: 'bg-panel text-normal hover:bg-tertiary disabled:opacity-50'
|
||||
}`}
|
||||
>
|
||||
{isPushPending ? (
|
||||
<SpinnerGapIcon className="size-icon-xs animate-spin" />
|
||||
) : isPushSuccess ? (
|
||||
<CheckCircleIcon className="size-icon-xs" weight="fill" />
|
||||
) : isPushError ? (
|
||||
<WarningCircleIcon className="size-icon-xs" weight="fill" />
|
||||
) : (
|
||||
<ArrowUpIcon className="size-icon-xs" weight="bold" />
|
||||
)}
|
||||
{isPushPending
|
||||
? t('git.states.pushing')
|
||||
: isPushSuccess
|
||||
? t('git.states.pushed')
|
||||
: isPushError
|
||||
? t('git.states.pushFailed')
|
||||
: t('git.states.push')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -16,12 +16,17 @@ export interface RepoInfo {
|
||||
name: string;
|
||||
targetBranch: string;
|
||||
commitsAhead: number;
|
||||
remoteCommitsAhead?: number;
|
||||
filesChanged: number;
|
||||
linesAdded: number;
|
||||
linesRemoved: number;
|
||||
prNumber?: number;
|
||||
prUrl?: string;
|
||||
prStatus?: 'open' | 'merged' | 'closed' | 'unknown';
|
||||
showPushButton?: boolean;
|
||||
isPushPending?: boolean;
|
||||
isPushSuccess?: boolean;
|
||||
isPushError?: boolean;
|
||||
}
|
||||
|
||||
interface GitPanelProps {
|
||||
@@ -29,6 +34,7 @@ interface GitPanelProps {
|
||||
workingBranchName: string;
|
||||
onWorkingBranchNameChange: (name: string) => void;
|
||||
onActionsClick?: (repoId: string, action: RepoAction) => void;
|
||||
onPushClick?: (repoId: string) => void;
|
||||
onOpenInEditor?: (repoId: string) => void;
|
||||
onCopyPath?: (repoId: string) => void;
|
||||
onAddRepo?: () => void;
|
||||
@@ -41,6 +47,7 @@ export function GitPanel({
|
||||
workingBranchName,
|
||||
onWorkingBranchNameChange,
|
||||
onActionsClick,
|
||||
onPushClick,
|
||||
onOpenInEditor,
|
||||
onCopyPath,
|
||||
className,
|
||||
@@ -75,9 +82,14 @@ export function GitPanel({
|
||||
prNumber={repo.prNumber}
|
||||
prUrl={repo.prUrl}
|
||||
prStatus={repo.prStatus}
|
||||
showPushButton={repo.showPushButton}
|
||||
isPushPending={repo.isPushPending}
|
||||
isPushSuccess={repo.isPushSuccess}
|
||||
isPushError={repo.isPushError}
|
||||
onChangeTarget={() => onActionsClick?.(repo.id, 'change-target')}
|
||||
onRebase={() => onActionsClick?.(repo.id, 'rebase')}
|
||||
onActionsClick={(action) => onActionsClick?.(repo.id, action)}
|
||||
onPushClick={() => onPushClick?.(repo.id)}
|
||||
onOpenInEditor={() => onOpenInEditor?.(repo.id)}
|
||||
onCopyPath={() => onCopyPath?.(repo.id)}
|
||||
/>
|
||||
|
||||
@@ -352,6 +352,7 @@
|
||||
"pushed": "Pushed!",
|
||||
"pushing": "Pushing...",
|
||||
"push": "Push",
|
||||
"pushFailed": "Failed",
|
||||
"forcePush": "Force Push",
|
||||
"forcePushing": "Force Pushing...",
|
||||
"creating": "Creating...",
|
||||
|
||||
@@ -309,6 +309,7 @@
|
||||
"push": "Enviar",
|
||||
"pushed": "¡Enviado!",
|
||||
"pushing": "Enviando...",
|
||||
"pushFailed": "Falló",
|
||||
"rebase": "Rebase",
|
||||
"rebasing": "Rebaseando..."
|
||||
},
|
||||
|
||||
@@ -307,6 +307,7 @@
|
||||
"push": "プッシュ",
|
||||
"pushed": "プッシュ完了!",
|
||||
"pushing": "プッシュ中...",
|
||||
"pushFailed": "失敗",
|
||||
"forcePush": "強制プッシュ",
|
||||
"forcePushing": "強制プッシュ中...",
|
||||
"rebase": "リベース",
|
||||
|
||||
@@ -263,6 +263,7 @@
|
||||
"pushed": "푸시됨!",
|
||||
"pushing": "푸시 중...",
|
||||
"push": "푸시",
|
||||
"pushFailed": "실패",
|
||||
"forcePush": "강제 푸시",
|
||||
"forcePushing": "강제 푸시 중...",
|
||||
"creating": "생성 중...",
|
||||
|
||||
@@ -298,6 +298,7 @@
|
||||
"pushed": "已推送!",
|
||||
"pushing": "推送中...",
|
||||
"push": "推送",
|
||||
"pushFailed": "失败",
|
||||
"forcePush": "强制推送",
|
||||
"forcePushing": "强制推送中...",
|
||||
"creating": "创建中...",
|
||||
|
||||
@@ -298,6 +298,7 @@
|
||||
"pushed": "已推送!",
|
||||
"pushing": "推送中...",
|
||||
"push": "推送",
|
||||
"pushFailed": "失敗",
|
||||
"forcePush": "強制推送",
|
||||
"forcePushing": "強制推送中...",
|
||||
"creating": "建立中...",
|
||||
|
||||
Reference in New Issue
Block a user