From a397d411dd70bc349955424137fd9ea89bf19357 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Tue, 23 Jun 2026 17:50:53 +0800 Subject: [PATCH] Improve GitHub panel toolbar and loading states (#1664) * feat(app): add toolbar and loading states to the GitHub PR panel Adds a toolbar under the PR title with a refresh button (refreshes git and GitHub status + timeline in one tap), an always-visible View Pull Request button, and a Merge control with a method dropdown that is disabled-with-reason when the PR can't be merged yet. The merge control reuses the existing git-actions policy verbatim via a new buildPrPanelMergeActions selector fed into GitActionsSplitButton, so no merge readiness/squash/auto-merge logic is duplicated. Also replaces the empty-state flash with skeletons: a full-panel skeleton while PR status loads and an activity skeleton while the timeline loads, with empty states shown only when loaded-and-empty and an error state with retry. * refactor(app): tidy PR panel skeleton (i18n, shared pulse, index) Addresses review feedback on the PR panel work: - Localize the skeleton's "Checks" label via t() instead of hardcoding. - Hoist the shared SkeletonPulse + useSkeletonPulse into activity-skeleton so pane-skeleton reuses them instead of duplicating the pulse driver. - Export PullRequestPaneError/Skeleton from the module index and import them through it, matching the module's existing public surface. * test(app): remove mock-based PR pane test * fix(app): simplify PR panel toolbar * fix(app): report PR panel retry failures --- .../app/src/components/explorer-sidebar.tsx | 57 ++++++- .../pull-request-panel/activity-skeleton.tsx | 87 ++++++++++ .../app/src/git/pull-request-panel/index.ts | 2 + .../src/git/pull-request-panel/pane-error.tsx | 39 +++++ .../git/pull-request-panel/pane-skeleton.tsx | 113 +++++++++++++ .../app/src/git/pull-request-panel/pane.tsx | 159 +++++++++++++++--- .../git/pull-request-panel/use-data.test.ts | 31 ++++ .../src/git/pull-request-panel/use-data.ts | 8 +- packages/app/src/git/use-actions.tsx | 120 ++++++------- packages/app/src/i18n/resources.test.ts | 1 + packages/app/src/i18n/resources/ar.ts | 3 + packages/app/src/i18n/resources/en.ts | 3 + packages/app/src/i18n/resources/es.ts | 3 + packages/app/src/i18n/resources/fr.ts | 3 + packages/app/src/i18n/resources/ru.ts | 3 + packages/app/src/i18n/resources/zh-CN.ts | 3 + 16 files changed, 550 insertions(+), 85 deletions(-) create mode 100644 packages/app/src/git/pull-request-panel/activity-skeleton.tsx create mode 100644 packages/app/src/git/pull-request-panel/pane-error.tsx create mode 100644 packages/app/src/git/pull-request-panel/pane-skeleton.tsx diff --git a/packages/app/src/components/explorer-sidebar.tsx b/packages/app/src/components/explorer-sidebar.tsx index aa9217ef1..0e73d89f3 100644 --- a/packages/app/src/components/explorer-sidebar.tsx +++ b/packages/app/src/components/explorer-sidebar.tsx @@ -15,9 +15,13 @@ import { useTranslation } from "react-i18next"; import { formatPrTabLabel, PullRequestPane, + PullRequestPaneError, + PullRequestPaneSkeleton, PullRequestTabIcon, usePrPaneData, } from "@/git/pull-request-panel"; +import { useCheckoutGitActionsStore } from "@/git/actions-store"; +import type { UsePrPaneDataResult } from "@/git/pull-request-panel/use-data"; import { usePanelStore, selectIsFileExplorerOpen, @@ -27,6 +31,7 @@ import { } from "@/stores/panel-store"; import { useExplorerSidebarAnimation } from "@/contexts/explorer-sidebar-animation-context"; import { useSidebarAnimation } from "@/contexts/sidebar-animation-context"; +import { useToast } from "@/contexts/toast-context"; import { canCloseRightSidebarGesture } from "@/utils/sidebar-animation-state"; import { HEADER_INNER_HEIGHT } from "@/constants/layout"; import { GitDiffPane } from "@/git/diff-pane"; @@ -452,6 +457,7 @@ function ExplorerSidebarContent({ }: SidebarContentProps) { const { theme } = useUnistyles(); const { t } = useTranslation(); + const toast = useToast(); const padding = useWindowControlsPadding("explorerSidebar"); const canQueryPullRequest = isGit && Boolean(workspaceRoot); const prPane = usePrPaneData({ @@ -461,11 +467,17 @@ function ExplorerSidebarContent({ timelineEnabled: activeTab === "pr" && canQueryPullRequest && isOpen, }); const hasPullRequest = prPane.prNumber !== null; + const showPrTab = hasPullRequest || (activeTab === "pr" && prPane.isLoading); const requestedTab: ExplorerTab = !isGit && (activeTab === "changes" || activeTab === "pr") ? "files" : activeTab; - const resolvedTab: ExplorerTab = - requestedTab === "pr" && !hasPullRequest ? "changes" : requestedTab; + const resolvedTab: ExplorerTab = requestedTab === "pr" && !showPrTab ? "changes" : requestedTab; const prTabLabel = formatPrTabLabel(prPane.prNumber); + const refreshGitActions = useCheckoutGitActionsStore((s) => s.refresh); + const handlePrRetry = useCallback(() => { + refreshGitActions({ serverId, cwd: workspaceRoot }).catch((error) => { + toast.error(error instanceof Error ? error.message : t("workspace.git.diff.failedRefresh")); + }); + }, [refreshGitActions, serverId, t, toast, workspaceRoot]); const workspaceAttachmentScopeKey = useMemo( () => buildWorkspaceAttachmentScopeKey({ serverId, workspaceId, cwd: workspaceRoot }), [serverId, workspaceId, workspaceRoot], @@ -498,7 +510,7 @@ function ExplorerSidebarContent({ onTabPress={onTabPress} testID="explorer-tab-files" /> - {isGit && hasPullRequest && ( + {isGit && showPrTab && ( )} - {resolvedTab === "pr" && prPane.data && ( - )} @@ -555,6 +568,38 @@ function ExplorerSidebarContent({ ); } +interface PrTabContentProps { + serverId: string; + cwd: string; + prPane: UsePrPaneDataResult; + workspaceAttachmentScopeKey: string; + onRetry: () => void; +} + +function PrTabContent({ + serverId, + cwd, + prPane, + workspaceAttachmentScopeKey, + onRetry, +}: PrTabContentProps) { + if (prPane.data) { + return ( + + ); + } + if (prPane.error) { + return ; + } + return ; +} + // Static styles for Animated.Views — must NOT use Unistyles dynamic theme to // avoid the "Unable to find node on an unmounted component" crash when Unistyles // tries to patch the native node that Reanimated also manages. diff --git a/packages/app/src/git/pull-request-panel/activity-skeleton.tsx b/packages/app/src/git/pull-request-panel/activity-skeleton.tsx new file mode 100644 index 000000000..72c315f3b --- /dev/null +++ b/packages/app/src/git/pull-request-panel/activity-skeleton.tsx @@ -0,0 +1,87 @@ +import { useEffect, useMemo, useRef } from "react"; +import { Animated, View, type StyleProp, type ViewStyle } from "react-native"; +import { StyleSheet } from "react-native-unistyles"; + +const ROW_KEYS = [0, 1, 2].map((i) => `pr-activity-skeleton-row-${i}`); + +export function useSkeletonPulse(): Animated.Value { + const pulse = useRef(new Animated.Value(0)).current; + useEffect(() => { + const animation = Animated.loop( + Animated.sequence([ + Animated.timing(pulse, { toValue: 1, duration: 1000, useNativeDriver: true }), + Animated.timing(pulse, { toValue: 0, duration: 1000, useNativeDriver: true }), + ]), + ); + animation.start(); + return () => animation.stop(); + }, [pulse]); + return pulse; +} + +export function SkeletonPulse({ + pulse, + style, +}: { + pulse: Animated.Value; + style: StyleProp; +}) { + const opacity = pulse.interpolate({ + inputRange: [0, 1], + outputRange: [0.4, 0.8], + }); + const pulseStyle = useMemo(() => [style, { opacity }], [style, opacity]); + return ; +} + +export function PrActivitySkeleton() { + const pulse = useSkeletonPulse(); + + return ( + + {ROW_KEYS.map((key) => ( + + + + + + + + ))} + + ); +} + +const styles = StyleSheet.create((theme) => ({ + container: { + paddingHorizontal: theme.spacing[3], + gap: theme.spacing[3], + }, + row: { + flexDirection: "row", + alignItems: "center", + gap: theme.spacing[2], + }, + avatar: { + width: 20, + height: 20, + borderRadius: theme.borderRadius.full, + backgroundColor: theme.colors.surface2, + }, + lines: { + flex: 1, + gap: theme.spacing[1], + }, + lineWide: { + width: "70%", + height: 12, + borderRadius: theme.borderRadius.sm, + backgroundColor: theme.colors.surface2, + }, + lineNarrow: { + width: "45%", + height: 10, + borderRadius: theme.borderRadius.sm, + backgroundColor: theme.colors.surface2, + }, +})); diff --git a/packages/app/src/git/pull-request-panel/index.ts b/packages/app/src/git/pull-request-panel/index.ts index 0f7e4a820..065625c31 100644 --- a/packages/app/src/git/pull-request-panel/index.ts +++ b/packages/app/src/git/pull-request-panel/index.ts @@ -1,4 +1,6 @@ export { PullRequestPane } from "./pane"; +export { PullRequestPaneError } from "./pane-error"; +export { PullRequestPaneSkeleton } from "./pane-skeleton"; export { PullRequestTabIcon } from "./tab-icon"; export { formatPrTabLabel } from "./tab-label"; export { prPaneTimelineQueryKind } from "./query-keys"; diff --git a/packages/app/src/git/pull-request-panel/pane-error.tsx b/packages/app/src/git/pull-request-panel/pane-error.tsx new file mode 100644 index 000000000..1e3cef516 --- /dev/null +++ b/packages/app/src/git/pull-request-panel/pane-error.tsx @@ -0,0 +1,39 @@ +import { Text, View } from "react-native"; +import { StyleSheet } from "react-native-unistyles"; +import { RotateCw } from "lucide-react-native"; +import { useTranslation } from "react-i18next"; +import { Button } from "@/components/ui/button"; + +export function PullRequestPaneError({ + onRetry, + message, +}: { + onRetry: () => void; + message?: string; +}) { + const { t } = useTranslation(); + return ( + + {message ?? t("workspace.git.diff.failedRefresh")} + + + ); +} + +const styles = StyleSheet.create((theme) => ({ + root: { + flex: 1, + alignItems: "center", + justifyContent: "center", + gap: theme.spacing[3], + paddingHorizontal: theme.spacing[4], + backgroundColor: theme.colors.surfaceSidebar, + }, + message: { + fontSize: theme.fontSize.xs, + color: theme.colors.foregroundMuted, + textAlign: "center", + }, +})); diff --git a/packages/app/src/git/pull-request-panel/pane-skeleton.tsx b/packages/app/src/git/pull-request-panel/pane-skeleton.tsx new file mode 100644 index 000000000..12e78dc16 --- /dev/null +++ b/packages/app/src/git/pull-request-panel/pane-skeleton.tsx @@ -0,0 +1,113 @@ +import { Text, View } from "react-native"; +import { StyleSheet } from "react-native-unistyles"; +import { useTranslation } from "react-i18next"; +import { PrActivitySkeleton, SkeletonPulse, useSkeletonPulse } from "./activity-skeleton"; + +const CHECK_ROW_KEYS = [0, 1, 2].map((i) => `pr-pane-skeleton-check-${i}`); + +export function PullRequestPaneSkeleton() { + const { t } = useTranslation(); + const pulse = useSkeletonPulse(); + + return ( + + + + + + + + + + + + + {t("workspace.git.pr.sections.checks")} + + {CHECK_ROW_KEYS.map((key) => ( + + + + + ))} + + + + + + + + ); +} + +const styles = StyleSheet.create((theme) => ({ + root: { + flex: 1, + minHeight: 0, + backgroundColor: theme.colors.surfaceSidebar, + }, + header: { + gap: theme.spacing[2], + paddingHorizontal: theme.spacing[4], + paddingVertical: theme.spacing[4], + }, + title: { + width: "75%", + height: 16, + borderRadius: theme.borderRadius.sm, + backgroundColor: theme.colors.surface2, + }, + subtitle: { + width: "40%", + height: 12, + borderRadius: theme.borderRadius.sm, + backgroundColor: theme.colors.surface2, + }, + toolbar: { + flexDirection: "row", + alignItems: "center", + gap: theme.spacing[2], + paddingHorizontal: theme.spacing[3], + paddingVertical: theme.spacing[2], + borderBottomWidth: 1, + borderBottomColor: theme.colors.border, + }, + toolbarButton: { + width: 96, + height: 24, + borderRadius: theme.borderRadius.lg, + backgroundColor: theme.colors.surface2, + }, + section: { + paddingVertical: theme.spacing[2], + }, + sectionTitle: { + fontSize: theme.fontSize.xs, + fontWeight: theme.fontWeight.medium, + color: theme.colors.foregroundMuted, + paddingHorizontal: theme.spacing[3], + paddingVertical: theme.spacing[2], + }, + checks: { + gap: theme.spacing[1], + }, + checkRow: { + flexDirection: "row", + alignItems: "center", + gap: theme.spacing[2], + paddingHorizontal: theme.spacing[3], + minHeight: 32, + }, + checkDot: { + width: 14, + height: 14, + borderRadius: theme.borderRadius.full, + backgroundColor: theme.colors.surface2, + }, + checkName: { + width: "60%", + height: 12, + borderRadius: theme.borderRadius.sm, + backgroundColor: theme.colors.surface2, + }, +})); diff --git a/packages/app/src/git/pull-request-panel/pane.tsx b/packages/app/src/git/pull-request-panel/pane.tsx index 4e1e10872..1f487af6d 100644 --- a/packages/app/src/git/pull-request-panel/pane.tsx +++ b/packages/app/src/git/pull-request-panel/pane.tsx @@ -25,10 +25,13 @@ import { MessageSquare, MessageSquarePlus, MoreHorizontal, + RotateCw, } from "lucide-react-native"; import type { PressableStateCallbackType } from "react-native"; +import { useTranslation } from "react-i18next"; import { openExternalUrl } from "@/utils/open-external-url"; import { Button } from "@/components/ui/button"; +import { LoadingSpinner } from "@/components/ui/loading-spinner"; import { StatusBadge } from "@/components/ui/status-badge"; import { DropdownMenu, @@ -42,9 +45,12 @@ import { writeMarkdownToRichClipboard } from "@/utils/rich-clipboard"; import { useHostRuntimeClient } from "@/runtime/host-runtime"; import { useSessionStore } from "@/stores/session-store"; import { useWorkspaceAttachmentsStore } from "@/attachments/workspace-attachments-store"; +import { useToast } from "@/contexts/toast-context"; +import { useCheckoutGitActionsStore } from "@/git/actions-store"; import { isNative } from "@/constants/platform"; -import { useIsCompactFormFactor } from "@/constants/layout"; -import type { Theme } from "@/styles/theme"; +import { useIsCompactFormFactor, WORKSPACE_SECONDARY_HEADER_HEIGHT } from "@/constants/layout"; +import { ICON_SIZE, type Theme } from "@/styles/theme"; +import { PrActivitySkeleton } from "./activity-skeleton"; import { collapseActivity, expandActivity, @@ -85,6 +91,8 @@ const ThemedGitPullRequestDraft = withUnistyles(GitPullRequestDraft); const ThemedMessageSquare = withUnistyles(MessageSquare); const ThemedMessageSquarePlus = withUnistyles(MessageSquarePlus); const ThemedMoreHorizontal = withUnistyles(MoreHorizontal); +const ThemedRotateCw = withUnistyles(RotateCw); +const ThemedLoadingSpinner = withUnistyles(LoadingSpinner); const foregroundColorMapping = (theme: Theme) => ({ color: theme.colors.foreground }); const foregroundMutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted }); @@ -138,6 +146,13 @@ function kebabTriggerStyle({ return [styles.kebabButton, hovered && styles.kebabButtonHovered]; } +function refreshButtonStyle({ + hovered = false, + pressed = false, +}: PressableStateCallbackType & { hovered?: boolean }) { + return [styles.refreshButton, (hovered || pressed) && styles.refreshButtonHovered]; +} + function renderKebabTriggerIcon({ hovered }: { hovered?: boolean }) { return ( state.sessions[serverId]?.serverInfo?.features?.githubCheckDetails === true, @@ -199,6 +218,24 @@ export function PullRequestPane({ void openExternalUrl(data.url); }, [data.url]); + const refreshSupported = useSessionStore( + (state) => state.sessions[serverId]?.serverInfo?.features?.checkoutRefresh === true, + ); + const runRefresh = useCheckoutGitActionsStore((state) => state.refresh); + const isRefreshing = + useCheckoutGitActionsStore((state) => + state.getStatus({ serverId, cwd, actionId: "refresh" }), + ) === "pending"; + + const handleRefresh = useCallback(() => { + if (isRefreshing) { + return; + } + void runRefresh({ serverId, cwd }).catch((error) => { + toast.error(error instanceof Error ? error.message : t("workspace.git.diff.failedRefresh")); + }); + }, [cwd, isRefreshing, runRefresh, serverId, t, toast]); + const handleToggleChecks = useCallback(() => { setChecksOpen((open) => !open); }, []); @@ -395,6 +432,47 @@ export function PullRequestPane({ return ( + + + + + {refreshSupported ? ( + + + {isRefreshing ? ( + + ) : ( + + )} + + + ) : null} + + {({ hovered }) => ( <> @@ -420,8 +498,6 @@ export function PullRequestPane({ )} - -
Add all to chat ) : null} - {visibleEntries.length === 0 ? ( + {activityLoading ? : null} + {!activityLoading && visibleEntries.length === 0 ? ( No activity yet - ) : ( - visibleEntries.map(({ entry, collapsed }) => ( - - )) - )} + ) : null} + {!activityLoading + ? visibleEntries.map(({ entry, collapsed }) => ( + + )) + : null}
@@ -1236,6 +1315,48 @@ const styles = StyleSheet.create((theme) => ({ height: 1, backgroundColor: theme.colors.border, }, + toolbar: { + height: WORKSPACE_SECONDARY_HEADER_HEIGHT, + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + borderBottomWidth: 1, + borderBottomColor: theme.colors.border, + paddingTop: theme.spacing[2], + paddingRight: theme.spacing[3], + paddingBottom: theme.spacing[2], + paddingLeft: theme.spacing[3], + }, + toolbarActions: { + flexDirection: "row", + alignItems: "center", + gap: theme.spacing[1], + }, + viewButton: { + gap: theme.spacing[1], + minHeight: 24, + height: 24, + paddingVertical: 0, + paddingHorizontal: theme.spacing[1], + borderRadius: theme.borderRadius.base, + }, + refreshButton: { + marginLeft: "auto", + width: 22, + height: 22, + borderRadius: theme.borderRadius.md, + alignItems: "center", + justifyContent: "center", + }, + refreshButtonHovered: { + backgroundColor: theme.colors.surface2, + }, + refreshIcon: { + width: ICON_SIZE.md, + height: ICON_SIZE.md, + alignItems: "center", + justifyContent: "center", + }, sectionHeader: { flexDirection: "row", alignItems: "center", diff --git a/packages/app/src/git/pull-request-panel/use-data.test.ts b/packages/app/src/git/pull-request-panel/use-data.test.ts index 11c1f1227..c3c5109dc 100644 --- a/packages/app/src/git/pull-request-panel/use-data.test.ts +++ b/packages/app/src/git/pull-request-panel/use-data.test.ts @@ -435,6 +435,37 @@ describe("selectPrPaneState", () => { expect(state.isLoading).toBe(false); }); + it("reports activityLoading while the timeline is pending its first response", () => { + const state = selectPrPaneState({ + ...baseSelectInput, + status: prStatus(), + shouldFetchTimeline: true, + timelineIsLoading: true, + }); + expect(state.activityLoading).toBe(true); + }); + + it("does not report activityLoading when no timeline fetch was scheduled", () => { + const state = selectPrPaneState({ + ...baseSelectInput, + status: prStatus(), + shouldFetchTimeline: false, + timelineIsLoading: true, + }); + expect(state.activityLoading).toBe(false); + }); + + it("does not report activityLoading once the timeline payload resolves", () => { + const state = selectPrPaneState({ + ...baseSelectInput, + status: prStatus(), + shouldFetchTimeline: true, + timelineIsLoading: false, + timelinePayload: timelinePayload(), + }); + expect(state.activityLoading).toBe(false); + }); + it("reports refreshing during background revalidation of either query", () => { expect( selectPrPaneState({ diff --git a/packages/app/src/git/pull-request-panel/use-data.ts b/packages/app/src/git/pull-request-panel/use-data.ts index e3cd9557e..b99cdfdf6 100644 --- a/packages/app/src/git/pull-request-panel/use-data.ts +++ b/packages/app/src/git/pull-request-panel/use-data.ts @@ -27,6 +27,7 @@ export interface UsePrPaneDataResult { data: PrPaneData | null; prNumber: number | null; isLoading: boolean; + activityLoading: boolean; isRefreshing: boolean; error: Error | null; githubFeaturesEnabled: boolean; @@ -167,13 +168,14 @@ export function selectPrPaneState(input: SelectPrPaneStateInput): UsePrPaneDataR : mapPrPaneData(input.status, input.timelinePayload); const statusRefreshing = input.statusIsFetching && !input.statusIsLoading; const timelineRefreshing = input.timelineIsFetching && !input.timelineIsLoading; + const timelinePending = + input.shouldFetchTimeline && input.timelineIsLoading && input.timelinePayload === undefined; return { data, prNumber: identity.prNumber, - isLoading: - input.statusIsLoading || - (input.shouldFetchTimeline && input.timelineIsLoading && input.timelinePayload === undefined), + isLoading: input.statusIsLoading || timelinePending, + activityLoading: timelinePending, isRefreshing: statusRefreshing || timelineRefreshing, error: firstNonSuppressedError({ statusPayloadError: input.statusPayloadError, diff --git a/packages/app/src/git/use-actions.tsx b/packages/app/src/git/use-actions.tsx index bdf74b323..22491d60f 100644 --- a/packages/app/src/git/use-actions.tsx +++ b/packages/app/src/git/use-actions.tsx @@ -7,6 +7,7 @@ import { type CheckoutPrStatusPayload, useCheckoutPrStatusQuery } from "@/git/us import { buildGitActions, narrowPullRequestState, + type BuildGitActionsInput, type GitAction, type GitActions, } from "@/git/policy"; @@ -568,8 +569,8 @@ export function useGitActions({ serverId, cwd, icons }: UseGitActionsInput): Use }, [prStatus?.url, handleCreatePr]); // Build actions - const gitActions: GitActions = useMemo(() => { - const actions = buildGitActions({ + const gitActionsInput = useMemo( + () => ({ isGit, githubFeaturesEnabled, githubAutoMergeActionsEnabled, @@ -684,61 +685,66 @@ export function useGitActions({ serverId, cwd, icons }: UseGitActionsInput): Use handler: handleArchiveWorktree, }, }, - }); - return translateGitActions(actions, { baseRefLabel, hasPullRequest, t }); - }, [ - t, - isGit, - hasRemote, - hasPullRequest, - prStatus?.url, - prStatus?.state, - prStatus?.isDraft, - prStatus?.isMerged, - prStatus?.mergeable, - prStatus?.github, - aheadCount, - behindBaseCount, - isPaseoOwnedWorktree, - isOnBaseBranch, - githubFeaturesEnabled, - githubAutoMergeActionsEnabled, - hasUncommittedChanges, - aheadOfOrigin, - behindOfOrigin, - shipDefault, - baseRefLabel, - shouldPromoteArchive, - actionsDisabled, - commitStatus, - pullStatus, - pushStatus, - pullAndPushStatus, - prCreateStatus, - mergePrStatuses.squash, - mergePrStatuses.merge, - mergePrStatuses.rebase, - enablePrAutoMergeStatuses.squash, - enablePrAutoMergeStatuses.merge, - enablePrAutoMergeStatuses.rebase, - disablePrAutoMergeStatus, - mergeStatus, - mergeFromBaseStatus, - archiveStatus, - handleCommit, - handlePull, - handlePush, - handlePullAndPush, - handlePrAction, - handleMergePr, - handleEnablePrAutoMerge, - handleDisablePrAutoMerge, - handleMergeBranch, - handleMergeFromBase, - handleArchiveWorktree, - icons, - baseRef, - ]); + }), + [ + isGit, + hasRemote, + hasPullRequest, + prStatus?.url, + prStatus?.state, + prStatus?.isDraft, + prStatus?.isMerged, + prStatus?.mergeable, + prStatus?.github, + aheadCount, + behindBaseCount, + isPaseoOwnedWorktree, + isOnBaseBranch, + githubFeaturesEnabled, + githubAutoMergeActionsEnabled, + hasUncommittedChanges, + aheadOfOrigin, + behindOfOrigin, + shipDefault, + baseRefLabel, + shouldPromoteArchive, + actionsDisabled, + commitStatus, + pullStatus, + pushStatus, + pullAndPushStatus, + prCreateStatus, + mergePrStatuses.squash, + mergePrStatuses.merge, + mergePrStatuses.rebase, + enablePrAutoMergeStatuses.squash, + enablePrAutoMergeStatuses.merge, + enablePrAutoMergeStatuses.rebase, + disablePrAutoMergeStatus, + mergeStatus, + mergeFromBaseStatus, + archiveStatus, + handleCommit, + handlePull, + handlePush, + handlePullAndPush, + handlePrAction, + handleMergePr, + handleEnablePrAutoMerge, + handleDisablePrAutoMerge, + handleMergeBranch, + handleMergeFromBase, + handleArchiveWorktree, + icons, + baseRef, + ], + ); + + const gitActions: GitActions = useMemo( + () => + translateGitActions(buildGitActions(gitActionsInput), { baseRefLabel, hasPullRequest, t }), + [gitActionsInput, baseRefLabel, hasPullRequest, t], + ); return { gitActions, branchLabel, isGit }; } diff --git a/packages/app/src/i18n/resources.test.ts b/packages/app/src/i18n/resources.test.ts index 42a56a4dc..aa935da05 100644 --- a/packages/app/src/i18n/resources.test.ts +++ b/packages/app/src/i18n/resources.test.ts @@ -243,6 +243,7 @@ describe("translation resources", () => { expect(en.workspace.git.actions.commit.label).toBe("Commit"); expect(en.workspace.git.diff.binaryFile).toBe("Binary file"); expect(en.workspace.git.pr.sections.checks).toBe("Checks"); + expect(en.workspace.git.pr.actions.viewPullRequest).toBe("View"); expect(en.review.comment.placeholder).toBe("Leave a comment"); }); diff --git a/packages/app/src/i18n/resources/ar.ts b/packages/app/src/i18n/resources/ar.ts index db1638913..9339ed33e 100644 --- a/packages/app/src/i18n/resources/ar.ts +++ b/packages/app/src/i18n/resources/ar.ts @@ -717,6 +717,9 @@ export const ar: TranslationResources = { failedOpen: "فشل في فتح مساحة العمل", }, pr: { + actions: { + viewPullRequest: "عرض", + }, sections: { checks: "الشيكات", reviews: "التعليقات", diff --git a/packages/app/src/i18n/resources/en.ts b/packages/app/src/i18n/resources/en.ts index 9a56ab44b..efca11b2b 100644 --- a/packages/app/src/i18n/resources/en.ts +++ b/packages/app/src/i18n/resources/en.ts @@ -724,6 +724,9 @@ export const en = { failedOpen: "Failed to open workspace", }, pr: { + actions: { + viewPullRequest: "View", + }, sections: { checks: "Checks", reviews: "Reviews", diff --git a/packages/app/src/i18n/resources/es.ts b/packages/app/src/i18n/resources/es.ts index 13cd887f1..95d572554 100644 --- a/packages/app/src/i18n/resources/es.ts +++ b/packages/app/src/i18n/resources/es.ts @@ -744,6 +744,9 @@ export const es: TranslationResources = { failedOpen: "No se pudo abrir el espacio de trabajo", }, pr: { + actions: { + viewPullRequest: "Ver", + }, sections: { checks: "cheques", reviews: "Reseñas", diff --git a/packages/app/src/i18n/resources/fr.ts b/packages/app/src/i18n/resources/fr.ts index 7cea39611..520b7c18b 100644 --- a/packages/app/src/i18n/resources/fr.ts +++ b/packages/app/src/i18n/resources/fr.ts @@ -743,6 +743,9 @@ export const fr: TranslationResources = { failedOpen: "Échec de l'ouverture de l'espace de travail", }, pr: { + actions: { + viewPullRequest: "Voir", + }, sections: { checks: "Chèques", reviews: "Avis", diff --git a/packages/app/src/i18n/resources/ru.ts b/packages/app/src/i18n/resources/ru.ts index c07851afb..872e7de65 100644 --- a/packages/app/src/i18n/resources/ru.ts +++ b/packages/app/src/i18n/resources/ru.ts @@ -736,6 +736,9 @@ export const ru: TranslationResources = { failedOpen: "Не удалось открыть рабочую область", }, pr: { + actions: { + viewPullRequest: "Открыть", + }, sections: { checks: "Чеки", reviews: "Отзывы", diff --git a/packages/app/src/i18n/resources/zh-CN.ts b/packages/app/src/i18n/resources/zh-CN.ts index 4c9b9ecf3..1958a2515 100644 --- a/packages/app/src/i18n/resources/zh-CN.ts +++ b/packages/app/src/i18n/resources/zh-CN.ts @@ -711,6 +711,9 @@ export const zhCN: TranslationResources = { failedOpen: "打开 workspace 失败", }, pr: { + actions: { + viewPullRequest: "查看", + }, sections: { checks: "Checks", reviews: "Reviews",