features: add commit and push option to git commit window

- Add split button with dropdown in commit dialog supporting both
  "Commit" and "Commit and Push" actions
- Pass folderId through gitCommit/gitPush API calls so backend emits
  events that the folder window can receive for toast notifications
- Update Tauri git_commit and git_push commands to accept folder_id
  parameter with window label fallback for cross-window compatibility
- Wrap commit page with GitCredentialProvider for push authentication
- Keep commit window open when push fails so user can see the error
- Add i18n translations for all 10 locales

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
xintaofei
2026-04-02 16:22:41 +08:00
parent 1287dab37c
commit efd8e1104f
16 changed files with 184 additions and 82 deletions

View File

@@ -1,7 +1,7 @@
"use client"
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { Check, ChevronDown, ChevronRight, Loader2 } from "lucide-react"
import { Check, ChevronDown, ChevronRight, Loader2, Upload } from "lucide-react"
import { useTranslations } from "next-intl"
import { Button } from "@/components/ui/button"
import { ScrollArea } from "@/components/ui/scroll-area"
@@ -36,12 +36,23 @@ import {
import {
gitAddFiles,
gitCommit,
gitPush,
gitRollbackFile,
gitShowFile,
gitStatus,
deleteFileTreeEntry,
readFilePreview,
} from "@/lib/api"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {
useGitCredential,
type GitRemoteHint,
} from "@/contexts/git-credential-context"
import type { GitStatusEntry } from "@/lib/types"
import { cn } from "@/lib/utils"
import { toast } from "sonner"
@@ -51,6 +62,7 @@ import { toErrorMessage } from "@/lib/app-error"
interface CommitWorkspaceProps {
folderPath: string
folderId?: number | null
onCommitted?: () => void
onCancel?: () => void
}
@@ -209,11 +221,13 @@ const CONFIRM_INITIAL: ConfirmState = {
export function CommitWorkspace({
folderPath,
folderId,
onCommitted,
onCancel,
}: CommitWorkspaceProps) {
const t = useTranslations("Folder.commitDialog")
const tCommon = useTranslations("Folder.common")
const { withCredentialRetry } = useGitCredential()
const [entries, setEntries] = useState<GitStatusEntry[]>([])
const containerRef = useRef<HTMLDivElement>(null)
const [containerWidth, setContainerWidth] = useState(0)
@@ -415,29 +429,49 @@ export function CommitWorkspace({
[filePathSet, handleViewDiff]
)
const handleCommit = useCallback(async () => {
const commitMessage = messageRef.current.trim()
if (!commitMessage || selected.size === 0 || !folderPath) return
setCommitting(true)
setError(null)
try {
const result = await gitCommit(
folderPath,
commitMessage,
Array.from(selected)
)
toast.success(t("toasts.commitCompleted"), {
description: t("toasts.committedFiles", {
count: result.committed_files,
}),
})
onCommitted?.()
} catch (err) {
setError(toErrorMessage(err))
} finally {
setCommitting(false)
}
}, [folderPath, onCommitted, selected, t])
const handleCommit = useCallback(
async (andPush?: boolean) => {
const commitMessage = messageRef.current.trim()
if (!commitMessage || selected.size === 0 || !folderPath) return
setCommitting(true)
setError(null)
try {
const result = await gitCommit(
folderPath,
commitMessage,
Array.from(selected),
folderId
)
toast.success(t("toasts.commitCompleted"), {
description: t("toasts.committedFiles", {
count: result.committed_files,
}),
})
if (andPush) {
try {
const hint: GitRemoteHint = { folderPath }
await withCredentialRetry(
(creds) => gitPush(folderPath, null, creds, folderId),
hint
)
} catch (pushErr) {
toast.error(t("toasts.pushFailed"), {
description: toErrorMessage(pushErr),
})
return
}
}
onCommitted?.()
} catch (err) {
setError(toErrorMessage(err))
} finally {
setCommitting(false)
}
},
[folderId, folderPath, onCommitted, selected, t, withCredentialRetry]
)
// --- Context menu actions ---
@@ -1168,15 +1202,42 @@ export function CommitWorkspace({
<Button variant="outline" onClick={onCancel}>
{tCommon("cancel")}
</Button>
<Button
disabled={committing || !hasMessage || selected.size === 0}
onClick={handleCommit}
>
{committing && (
<Loader2 className="mr-1 h-4 w-4 animate-spin" />
)}
{t("commitButton", { count: selected.size })}
</Button>
<div className="flex">
<Button
disabled={committing || !hasMessage || selected.size === 0}
onClick={() => handleCommit()}
className="rounded-r-none"
>
{committing && (
<Loader2 className="mr-1 h-4 w-4 animate-spin" />
)}
{t("commitButton", { count: selected.size })}
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
disabled={
committing || !hasMessage || selected.size === 0
}
className="rounded-l-none border-l border-l-primary-foreground/20 px-1.5"
>
<ChevronDown className="h-3.5 w-3.5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => handleCommit()}>
<Check className="h-4 w-4" />
{t("commitButton", { count: selected.size })}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleCommit(true)}>
<Upload className="h-4 w-4" />
{t("commitAndPushButton", {
count: selected.size,
})}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</div>
</div>

View File

@@ -278,12 +278,14 @@ function parseDate(dateStr: string): Date | null {
interface PushWorkspaceProps {
folderPath: string
folderName: string
folderId?: number | null
onPushed?: () => void
}
export function PushWorkspace({
folderPath,
folderName,
folderId,
onPushed,
}: PushWorkspaceProps) {
const t = useTranslations("Folder.pushWindow")
@@ -392,7 +394,7 @@ export function PushWorkspace({
)?.url
const hint: GitRemoteHint = remoteUrl ? { remoteUrl } : { folderPath }
await withCredentialRetry(
(creds) => gitPush(folderPath, selectedRemote, creds),
(creds) => gitPush(folderPath, selectedRemote, creds, folderId),
hint
)
onPushed?.()