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
This commit is contained in:
Mohamed Boudra
2026-06-23 17:50:53 +08:00
committed by GitHub
parent d9cd6ea0fd
commit a397d411dd
16 changed files with 550 additions and 85 deletions
@@ -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 && (
<ExplorerTabButton
tab="pr"
active={resolvedTab === "pr"}
@@ -542,12 +554,13 @@ function ExplorerSidebarContent({
onOpenFile={onOpenFile}
/>
)}
{resolvedTab === "pr" && prPane.data && (
<PullRequestPane
{resolvedTab === "pr" && (
<PrTabContent
serverId={serverId}
cwd={workspaceRoot}
data={prPane.data}
prPane={prPane}
workspaceAttachmentScopeKey={workspaceAttachmentScopeKey}
onRetry={handlePrRetry}
/>
)}
</View>
@@ -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 (
<PullRequestPane
serverId={serverId}
cwd={cwd}
data={prPane.data}
activityLoading={prPane.activityLoading}
workspaceAttachmentScopeKey={workspaceAttachmentScopeKey}
/>
);
}
if (prPane.error) {
return <PullRequestPaneError onRetry={onRetry} />;
}
return <PullRequestPaneSkeleton />;
}
// 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.
@@ -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<ViewStyle>;
}) {
const opacity = pulse.interpolate({
inputRange: [0, 1],
outputRange: [0.4, 0.8],
});
const pulseStyle = useMemo(() => [style, { opacity }], [style, opacity]);
return <Animated.View style={pulseStyle} />;
}
export function PrActivitySkeleton() {
const pulse = useSkeletonPulse();
return (
<View style={styles.container} testID="pr-pane-activity-skeleton">
{ROW_KEYS.map((key) => (
<View key={key} style={styles.row}>
<SkeletonPulse pulse={pulse} style={styles.avatar} />
<View style={styles.lines}>
<SkeletonPulse pulse={pulse} style={styles.lineWide} />
<SkeletonPulse pulse={pulse} style={styles.lineNarrow} />
</View>
</View>
))}
</View>
);
}
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,
},
}));
@@ -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";
@@ -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 (
<View style={styles.root} testID="pr-pane-error">
<Text style={styles.message}>{message ?? t("workspace.git.diff.failedRefresh")}</Text>
<Button variant="ghost" size="xs" leftIcon={RotateCw} onPress={onRetry}>
{t("common.actions.retry")}
</Button>
</View>
);
}
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",
},
}));
@@ -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 (
<View style={styles.root} testID="pr-pane-skeleton">
<View style={styles.header}>
<SkeletonPulse pulse={pulse} style={styles.title} />
<SkeletonPulse pulse={pulse} style={styles.subtitle} />
</View>
<View style={styles.toolbar}>
<SkeletonPulse pulse={pulse} style={styles.toolbarButton} />
<SkeletonPulse pulse={pulse} style={styles.toolbarButton} />
</View>
<View style={styles.section}>
<Text style={styles.sectionTitle}>{t("workspace.git.pr.sections.checks")}</Text>
<View style={styles.checks}>
{CHECK_ROW_KEYS.map((key) => (
<View key={key} style={styles.checkRow}>
<SkeletonPulse pulse={pulse} style={styles.checkDot} />
<SkeletonPulse pulse={pulse} style={styles.checkName} />
</View>
))}
</View>
</View>
<View style={styles.section}>
<PrActivitySkeleton />
</View>
</View>
);
}
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,
},
}));
+140 -19
View File
@@ -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 (
<ThemedMoreHorizontal
@@ -176,13 +191,17 @@ export function PullRequestPane({
serverId,
cwd,
data,
activityLoading,
workspaceAttachmentScopeKey,
}: {
serverId: string;
cwd: string;
data: PrPaneData;
activityLoading: boolean;
workspaceAttachmentScopeKey?: string;
}) {
const { t } = useTranslation();
const toast = useToast();
const daemonClient = useHostRuntimeClient(serverId);
const canFetchGitHubCheckDetails = useSessionStore(
(state) => 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 (
<View style={styles.root} testID="pr-pane">
<ScrollView style={styles.scroll} showsVerticalScrollIndicator={false}>
<View style={styles.toolbar} testID="pr-pane-toolbar">
<View style={styles.toolbarActions}>
<Button
variant="ghost"
size="xs"
leftIcon={ExternalLink}
onPress={handleOpenPrUrl}
style={styles.viewButton}
testID="pr-pane-view-pr"
>
{t("workspace.git.pr.actions.viewPullRequest")}
</Button>
</View>
{refreshSupported ? (
<Pressable
accessibilityRole="button"
accessibilityLabel={
isRefreshing
? t("workspace.git.diff.refreshing")
: t("workspace.git.diff.refreshState")
}
testID="pr-pane-refresh"
style={refreshButtonStyle}
hitSlop={8}
onPress={handleRefresh}
disabled={isRefreshing}
>
<View style={styles.refreshIcon}>
{isRefreshing ? (
<ThemedLoadingSpinner
size={ICON_SIZE.sm}
uniProps={foregroundMutedColorMapping}
/>
) : (
<ThemedRotateCw size={ICON_SIZE.sm} uniProps={foregroundMutedColorMapping} />
)}
</View>
</Pressable>
) : null}
</View>
<Pressable onPress={handleOpenPrUrl} style={styles.header}>
{({ hovered }) => (
<>
@@ -420,8 +498,6 @@ export function PullRequestPane({
)}
</Pressable>
<View style={styles.divider} />
<Section
title="Checks"
open={checksOpen}
@@ -488,27 +564,30 @@ export function PullRequestPane({
size="xs"
leftIcon={MessageSquarePlus}
onPress={handleAddAllToChat}
disabled={activityLoading}
>
Add all to chat
</Button>
</View>
) : null}
{visibleEntries.length === 0 ? (
{activityLoading ? <PrActivitySkeleton /> : null}
{!activityLoading && visibleEntries.length === 0 ? (
<Text style={styles.emptyText}>No activity yet</Text>
) : (
visibleEntries.map(({ entry, collapsed }) => (
<TimelineEntryCard
key={entry.id}
entry={entry}
collapsed={collapsed}
collapsedEntryIds={collapsedEntryIds}
attachEnabled={attachEnabled}
onAddToChat={handleAddActivityToChat}
onAddThreadToChat={handleAddThreadToChat}
onToggleCollapsed={handleToggleEntryCollapsed}
/>
))
)}
) : null}
{!activityLoading
? visibleEntries.map(({ entry, collapsed }) => (
<TimelineEntryCard
key={entry.id}
entry={entry}
collapsed={collapsed}
collapsedEntryIds={collapsedEntryIds}
attachEnabled={attachEnabled}
onAddToChat={handleAddActivityToChat}
onAddThreadToChat={handleAddThreadToChat}
onToggleCollapsed={handleToggleEntryCollapsed}
/>
))
: null}
</Section>
</ScrollView>
</View>
@@ -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",
@@ -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({
@@ -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,
+63 -57
View File
@@ -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<BuildGitActionsInput>(
() => ({
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 };
}
+1
View File
@@ -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");
});
+3
View File
@@ -717,6 +717,9 @@ export const ar: TranslationResources = {
failedOpen: "فشل في فتح مساحة العمل",
},
pr: {
actions: {
viewPullRequest: "عرض",
},
sections: {
checks: "الشيكات",
reviews: "التعليقات",
+3
View File
@@ -724,6 +724,9 @@ export const en = {
failedOpen: "Failed to open workspace",
},
pr: {
actions: {
viewPullRequest: "View",
},
sections: {
checks: "Checks",
reviews: "Reviews",
+3
View File
@@ -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",
+3
View File
@@ -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",
+3
View File
@@ -736,6 +736,9 @@ export const ru: TranslationResources = {
failedOpen: "Не удалось открыть рабочую область",
},
pr: {
actions: {
viewPullRequest: "Открыть",
},
sections: {
checks: "Чеки",
reviews: "Отзывы",
+3
View File
@@ -711,6 +711,9 @@ export const zhCN: TranslationResources = {
failedOpen: "打开 workspace 失败",
},
pr: {
actions: {
viewPullRequest: "查看",
},
sections: {
checks: "Checks",
reviews: "Reviews",