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:
@@ -15,9 +15,13 @@ import { useTranslation } from "react-i18next";
|
|||||||
import {
|
import {
|
||||||
formatPrTabLabel,
|
formatPrTabLabel,
|
||||||
PullRequestPane,
|
PullRequestPane,
|
||||||
|
PullRequestPaneError,
|
||||||
|
PullRequestPaneSkeleton,
|
||||||
PullRequestTabIcon,
|
PullRequestTabIcon,
|
||||||
usePrPaneData,
|
usePrPaneData,
|
||||||
} from "@/git/pull-request-panel";
|
} from "@/git/pull-request-panel";
|
||||||
|
import { useCheckoutGitActionsStore } from "@/git/actions-store";
|
||||||
|
import type { UsePrPaneDataResult } from "@/git/pull-request-panel/use-data";
|
||||||
import {
|
import {
|
||||||
usePanelStore,
|
usePanelStore,
|
||||||
selectIsFileExplorerOpen,
|
selectIsFileExplorerOpen,
|
||||||
@@ -27,6 +31,7 @@ import {
|
|||||||
} from "@/stores/panel-store";
|
} from "@/stores/panel-store";
|
||||||
import { useExplorerSidebarAnimation } from "@/contexts/explorer-sidebar-animation-context";
|
import { useExplorerSidebarAnimation } from "@/contexts/explorer-sidebar-animation-context";
|
||||||
import { useSidebarAnimation } from "@/contexts/sidebar-animation-context";
|
import { useSidebarAnimation } from "@/contexts/sidebar-animation-context";
|
||||||
|
import { useToast } from "@/contexts/toast-context";
|
||||||
import { canCloseRightSidebarGesture } from "@/utils/sidebar-animation-state";
|
import { canCloseRightSidebarGesture } from "@/utils/sidebar-animation-state";
|
||||||
import { HEADER_INNER_HEIGHT } from "@/constants/layout";
|
import { HEADER_INNER_HEIGHT } from "@/constants/layout";
|
||||||
import { GitDiffPane } from "@/git/diff-pane";
|
import { GitDiffPane } from "@/git/diff-pane";
|
||||||
@@ -452,6 +457,7 @@ function ExplorerSidebarContent({
|
|||||||
}: SidebarContentProps) {
|
}: SidebarContentProps) {
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const toast = useToast();
|
||||||
const padding = useWindowControlsPadding("explorerSidebar");
|
const padding = useWindowControlsPadding("explorerSidebar");
|
||||||
const canQueryPullRequest = isGit && Boolean(workspaceRoot);
|
const canQueryPullRequest = isGit && Boolean(workspaceRoot);
|
||||||
const prPane = usePrPaneData({
|
const prPane = usePrPaneData({
|
||||||
@@ -461,11 +467,17 @@ function ExplorerSidebarContent({
|
|||||||
timelineEnabled: activeTab === "pr" && canQueryPullRequest && isOpen,
|
timelineEnabled: activeTab === "pr" && canQueryPullRequest && isOpen,
|
||||||
});
|
});
|
||||||
const hasPullRequest = prPane.prNumber !== null;
|
const hasPullRequest = prPane.prNumber !== null;
|
||||||
|
const showPrTab = hasPullRequest || (activeTab === "pr" && prPane.isLoading);
|
||||||
const requestedTab: ExplorerTab =
|
const requestedTab: ExplorerTab =
|
||||||
!isGit && (activeTab === "changes" || activeTab === "pr") ? "files" : activeTab;
|
!isGit && (activeTab === "changes" || activeTab === "pr") ? "files" : activeTab;
|
||||||
const resolvedTab: ExplorerTab =
|
const resolvedTab: ExplorerTab = requestedTab === "pr" && !showPrTab ? "changes" : requestedTab;
|
||||||
requestedTab === "pr" && !hasPullRequest ? "changes" : requestedTab;
|
|
||||||
const prTabLabel = formatPrTabLabel(prPane.prNumber);
|
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(
|
const workspaceAttachmentScopeKey = useMemo(
|
||||||
() => buildWorkspaceAttachmentScopeKey({ serverId, workspaceId, cwd: workspaceRoot }),
|
() => buildWorkspaceAttachmentScopeKey({ serverId, workspaceId, cwd: workspaceRoot }),
|
||||||
[serverId, workspaceId, workspaceRoot],
|
[serverId, workspaceId, workspaceRoot],
|
||||||
@@ -498,7 +510,7 @@ function ExplorerSidebarContent({
|
|||||||
onTabPress={onTabPress}
|
onTabPress={onTabPress}
|
||||||
testID="explorer-tab-files"
|
testID="explorer-tab-files"
|
||||||
/>
|
/>
|
||||||
{isGit && hasPullRequest && (
|
{isGit && showPrTab && (
|
||||||
<ExplorerTabButton
|
<ExplorerTabButton
|
||||||
tab="pr"
|
tab="pr"
|
||||||
active={resolvedTab === "pr"}
|
active={resolvedTab === "pr"}
|
||||||
@@ -542,12 +554,13 @@ function ExplorerSidebarContent({
|
|||||||
onOpenFile={onOpenFile}
|
onOpenFile={onOpenFile}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{resolvedTab === "pr" && prPane.data && (
|
{resolvedTab === "pr" && (
|
||||||
<PullRequestPane
|
<PrTabContent
|
||||||
serverId={serverId}
|
serverId={serverId}
|
||||||
cwd={workspaceRoot}
|
cwd={workspaceRoot}
|
||||||
data={prPane.data}
|
prPane={prPane}
|
||||||
workspaceAttachmentScopeKey={workspaceAttachmentScopeKey}
|
workspaceAttachmentScopeKey={workspaceAttachmentScopeKey}
|
||||||
|
onRetry={handlePrRetry}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</View>
|
</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
|
// 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
|
// avoid the "Unable to find node on an unmounted component" crash when Unistyles
|
||||||
// tries to patch the native node that Reanimated also manages.
|
// 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 { PullRequestPane } from "./pane";
|
||||||
|
export { PullRequestPaneError } from "./pane-error";
|
||||||
|
export { PullRequestPaneSkeleton } from "./pane-skeleton";
|
||||||
export { PullRequestTabIcon } from "./tab-icon";
|
export { PullRequestTabIcon } from "./tab-icon";
|
||||||
export { formatPrTabLabel } from "./tab-label";
|
export { formatPrTabLabel } from "./tab-label";
|
||||||
export { prPaneTimelineQueryKind } from "./query-keys";
|
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,
|
||||||
|
},
|
||||||
|
}));
|
||||||
@@ -25,10 +25,13 @@ import {
|
|||||||
MessageSquare,
|
MessageSquare,
|
||||||
MessageSquarePlus,
|
MessageSquarePlus,
|
||||||
MoreHorizontal,
|
MoreHorizontal,
|
||||||
|
RotateCw,
|
||||||
} from "lucide-react-native";
|
} from "lucide-react-native";
|
||||||
import type { PressableStateCallbackType } from "react-native";
|
import type { PressableStateCallbackType } from "react-native";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { openExternalUrl } from "@/utils/open-external-url";
|
import { openExternalUrl } from "@/utils/open-external-url";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { LoadingSpinner } from "@/components/ui/loading-spinner";
|
||||||
import { StatusBadge } from "@/components/ui/status-badge";
|
import { StatusBadge } from "@/components/ui/status-badge";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@@ -42,9 +45,12 @@ import { writeMarkdownToRichClipboard } from "@/utils/rich-clipboard";
|
|||||||
import { useHostRuntimeClient } from "@/runtime/host-runtime";
|
import { useHostRuntimeClient } from "@/runtime/host-runtime";
|
||||||
import { useSessionStore } from "@/stores/session-store";
|
import { useSessionStore } from "@/stores/session-store";
|
||||||
import { useWorkspaceAttachmentsStore } from "@/attachments/workspace-attachments-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 { isNative } from "@/constants/platform";
|
||||||
import { useIsCompactFormFactor } from "@/constants/layout";
|
import { useIsCompactFormFactor, WORKSPACE_SECONDARY_HEADER_HEIGHT } from "@/constants/layout";
|
||||||
import type { Theme } from "@/styles/theme";
|
import { ICON_SIZE, type Theme } from "@/styles/theme";
|
||||||
|
import { PrActivitySkeleton } from "./activity-skeleton";
|
||||||
import {
|
import {
|
||||||
collapseActivity,
|
collapseActivity,
|
||||||
expandActivity,
|
expandActivity,
|
||||||
@@ -85,6 +91,8 @@ const ThemedGitPullRequestDraft = withUnistyles(GitPullRequestDraft);
|
|||||||
const ThemedMessageSquare = withUnistyles(MessageSquare);
|
const ThemedMessageSquare = withUnistyles(MessageSquare);
|
||||||
const ThemedMessageSquarePlus = withUnistyles(MessageSquarePlus);
|
const ThemedMessageSquarePlus = withUnistyles(MessageSquarePlus);
|
||||||
const ThemedMoreHorizontal = withUnistyles(MoreHorizontal);
|
const ThemedMoreHorizontal = withUnistyles(MoreHorizontal);
|
||||||
|
const ThemedRotateCw = withUnistyles(RotateCw);
|
||||||
|
const ThemedLoadingSpinner = withUnistyles(LoadingSpinner);
|
||||||
|
|
||||||
const foregroundColorMapping = (theme: Theme) => ({ color: theme.colors.foreground });
|
const foregroundColorMapping = (theme: Theme) => ({ color: theme.colors.foreground });
|
||||||
const foregroundMutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted });
|
const foregroundMutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted });
|
||||||
@@ -138,6 +146,13 @@ function kebabTriggerStyle({
|
|||||||
return [styles.kebabButton, hovered && styles.kebabButtonHovered];
|
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 }) {
|
function renderKebabTriggerIcon({ hovered }: { hovered?: boolean }) {
|
||||||
return (
|
return (
|
||||||
<ThemedMoreHorizontal
|
<ThemedMoreHorizontal
|
||||||
@@ -176,13 +191,17 @@ export function PullRequestPane({
|
|||||||
serverId,
|
serverId,
|
||||||
cwd,
|
cwd,
|
||||||
data,
|
data,
|
||||||
|
activityLoading,
|
||||||
workspaceAttachmentScopeKey,
|
workspaceAttachmentScopeKey,
|
||||||
}: {
|
}: {
|
||||||
serverId: string;
|
serverId: string;
|
||||||
cwd: string;
|
cwd: string;
|
||||||
data: PrPaneData;
|
data: PrPaneData;
|
||||||
|
activityLoading: boolean;
|
||||||
workspaceAttachmentScopeKey?: string;
|
workspaceAttachmentScopeKey?: string;
|
||||||
}) {
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const toast = useToast();
|
||||||
const daemonClient = useHostRuntimeClient(serverId);
|
const daemonClient = useHostRuntimeClient(serverId);
|
||||||
const canFetchGitHubCheckDetails = useSessionStore(
|
const canFetchGitHubCheckDetails = useSessionStore(
|
||||||
(state) => state.sessions[serverId]?.serverInfo?.features?.githubCheckDetails === true,
|
(state) => state.sessions[serverId]?.serverInfo?.features?.githubCheckDetails === true,
|
||||||
@@ -199,6 +218,24 @@ export function PullRequestPane({
|
|||||||
void openExternalUrl(data.url);
|
void openExternalUrl(data.url);
|
||||||
}, [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(() => {
|
const handleToggleChecks = useCallback(() => {
|
||||||
setChecksOpen((open) => !open);
|
setChecksOpen((open) => !open);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -395,6 +432,47 @@ export function PullRequestPane({
|
|||||||
return (
|
return (
|
||||||
<View style={styles.root} testID="pr-pane">
|
<View style={styles.root} testID="pr-pane">
|
||||||
<ScrollView style={styles.scroll} showsVerticalScrollIndicator={false}>
|
<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}>
|
<Pressable onPress={handleOpenPrUrl} style={styles.header}>
|
||||||
{({ hovered }) => (
|
{({ hovered }) => (
|
||||||
<>
|
<>
|
||||||
@@ -420,8 +498,6 @@ export function PullRequestPane({
|
|||||||
)}
|
)}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
|
|
||||||
<View style={styles.divider} />
|
|
||||||
|
|
||||||
<Section
|
<Section
|
||||||
title="Checks"
|
title="Checks"
|
||||||
open={checksOpen}
|
open={checksOpen}
|
||||||
@@ -488,27 +564,30 @@ export function PullRequestPane({
|
|||||||
size="xs"
|
size="xs"
|
||||||
leftIcon={MessageSquarePlus}
|
leftIcon={MessageSquarePlus}
|
||||||
onPress={handleAddAllToChat}
|
onPress={handleAddAllToChat}
|
||||||
|
disabled={activityLoading}
|
||||||
>
|
>
|
||||||
Add all to chat
|
Add all to chat
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
{visibleEntries.length === 0 ? (
|
{activityLoading ? <PrActivitySkeleton /> : null}
|
||||||
|
{!activityLoading && visibleEntries.length === 0 ? (
|
||||||
<Text style={styles.emptyText}>No activity yet</Text>
|
<Text style={styles.emptyText}>No activity yet</Text>
|
||||||
) : (
|
) : null}
|
||||||
visibleEntries.map(({ entry, collapsed }) => (
|
{!activityLoading
|
||||||
<TimelineEntryCard
|
? visibleEntries.map(({ entry, collapsed }) => (
|
||||||
key={entry.id}
|
<TimelineEntryCard
|
||||||
entry={entry}
|
key={entry.id}
|
||||||
collapsed={collapsed}
|
entry={entry}
|
||||||
collapsedEntryIds={collapsedEntryIds}
|
collapsed={collapsed}
|
||||||
attachEnabled={attachEnabled}
|
collapsedEntryIds={collapsedEntryIds}
|
||||||
onAddToChat={handleAddActivityToChat}
|
attachEnabled={attachEnabled}
|
||||||
onAddThreadToChat={handleAddThreadToChat}
|
onAddToChat={handleAddActivityToChat}
|
||||||
onToggleCollapsed={handleToggleEntryCollapsed}
|
onAddThreadToChat={handleAddThreadToChat}
|
||||||
/>
|
onToggleCollapsed={handleToggleEntryCollapsed}
|
||||||
))
|
/>
|
||||||
)}
|
))
|
||||||
|
: null}
|
||||||
</Section>
|
</Section>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</View>
|
</View>
|
||||||
@@ -1236,6 +1315,48 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
height: 1,
|
height: 1,
|
||||||
backgroundColor: theme.colors.border,
|
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: {
|
sectionHeader: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
|||||||
@@ -435,6 +435,37 @@ describe("selectPrPaneState", () => {
|
|||||||
expect(state.isLoading).toBe(false);
|
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", () => {
|
it("reports refreshing during background revalidation of either query", () => {
|
||||||
expect(
|
expect(
|
||||||
selectPrPaneState({
|
selectPrPaneState({
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export interface UsePrPaneDataResult {
|
|||||||
data: PrPaneData | null;
|
data: PrPaneData | null;
|
||||||
prNumber: number | null;
|
prNumber: number | null;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
|
activityLoading: boolean;
|
||||||
isRefreshing: boolean;
|
isRefreshing: boolean;
|
||||||
error: Error | null;
|
error: Error | null;
|
||||||
githubFeaturesEnabled: boolean;
|
githubFeaturesEnabled: boolean;
|
||||||
@@ -167,13 +168,14 @@ export function selectPrPaneState(input: SelectPrPaneStateInput): UsePrPaneDataR
|
|||||||
: mapPrPaneData(input.status, input.timelinePayload);
|
: mapPrPaneData(input.status, input.timelinePayload);
|
||||||
const statusRefreshing = input.statusIsFetching && !input.statusIsLoading;
|
const statusRefreshing = input.statusIsFetching && !input.statusIsLoading;
|
||||||
const timelineRefreshing = input.timelineIsFetching && !input.timelineIsLoading;
|
const timelineRefreshing = input.timelineIsFetching && !input.timelineIsLoading;
|
||||||
|
const timelinePending =
|
||||||
|
input.shouldFetchTimeline && input.timelineIsLoading && input.timelinePayload === undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data,
|
data,
|
||||||
prNumber: identity.prNumber,
|
prNumber: identity.prNumber,
|
||||||
isLoading:
|
isLoading: input.statusIsLoading || timelinePending,
|
||||||
input.statusIsLoading ||
|
activityLoading: timelinePending,
|
||||||
(input.shouldFetchTimeline && input.timelineIsLoading && input.timelinePayload === undefined),
|
|
||||||
isRefreshing: statusRefreshing || timelineRefreshing,
|
isRefreshing: statusRefreshing || timelineRefreshing,
|
||||||
error: firstNonSuppressedError({
|
error: firstNonSuppressedError({
|
||||||
statusPayloadError: input.statusPayloadError,
|
statusPayloadError: input.statusPayloadError,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { type CheckoutPrStatusPayload, useCheckoutPrStatusQuery } from "@/git/us
|
|||||||
import {
|
import {
|
||||||
buildGitActions,
|
buildGitActions,
|
||||||
narrowPullRequestState,
|
narrowPullRequestState,
|
||||||
|
type BuildGitActionsInput,
|
||||||
type GitAction,
|
type GitAction,
|
||||||
type GitActions,
|
type GitActions,
|
||||||
} from "@/git/policy";
|
} from "@/git/policy";
|
||||||
@@ -568,8 +569,8 @@ export function useGitActions({ serverId, cwd, icons }: UseGitActionsInput): Use
|
|||||||
}, [prStatus?.url, handleCreatePr]);
|
}, [prStatus?.url, handleCreatePr]);
|
||||||
|
|
||||||
// Build actions
|
// Build actions
|
||||||
const gitActions: GitActions = useMemo(() => {
|
const gitActionsInput = useMemo<BuildGitActionsInput>(
|
||||||
const actions = buildGitActions({
|
() => ({
|
||||||
isGit,
|
isGit,
|
||||||
githubFeaturesEnabled,
|
githubFeaturesEnabled,
|
||||||
githubAutoMergeActionsEnabled,
|
githubAutoMergeActionsEnabled,
|
||||||
@@ -684,61 +685,66 @@ export function useGitActions({ serverId, cwd, icons }: UseGitActionsInput): Use
|
|||||||
handler: handleArchiveWorktree,
|
handler: handleArchiveWorktree,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
}),
|
||||||
return translateGitActions(actions, { baseRefLabel, hasPullRequest, t });
|
[
|
||||||
}, [
|
isGit,
|
||||||
t,
|
hasRemote,
|
||||||
isGit,
|
hasPullRequest,
|
||||||
hasRemote,
|
prStatus?.url,
|
||||||
hasPullRequest,
|
prStatus?.state,
|
||||||
prStatus?.url,
|
prStatus?.isDraft,
|
||||||
prStatus?.state,
|
prStatus?.isMerged,
|
||||||
prStatus?.isDraft,
|
prStatus?.mergeable,
|
||||||
prStatus?.isMerged,
|
prStatus?.github,
|
||||||
prStatus?.mergeable,
|
aheadCount,
|
||||||
prStatus?.github,
|
behindBaseCount,
|
||||||
aheadCount,
|
isPaseoOwnedWorktree,
|
||||||
behindBaseCount,
|
isOnBaseBranch,
|
||||||
isPaseoOwnedWorktree,
|
githubFeaturesEnabled,
|
||||||
isOnBaseBranch,
|
githubAutoMergeActionsEnabled,
|
||||||
githubFeaturesEnabled,
|
hasUncommittedChanges,
|
||||||
githubAutoMergeActionsEnabled,
|
aheadOfOrigin,
|
||||||
hasUncommittedChanges,
|
behindOfOrigin,
|
||||||
aheadOfOrigin,
|
shipDefault,
|
||||||
behindOfOrigin,
|
baseRefLabel,
|
||||||
shipDefault,
|
shouldPromoteArchive,
|
||||||
baseRefLabel,
|
actionsDisabled,
|
||||||
shouldPromoteArchive,
|
commitStatus,
|
||||||
actionsDisabled,
|
pullStatus,
|
||||||
commitStatus,
|
pushStatus,
|
||||||
pullStatus,
|
pullAndPushStatus,
|
||||||
pushStatus,
|
prCreateStatus,
|
||||||
pullAndPushStatus,
|
mergePrStatuses.squash,
|
||||||
prCreateStatus,
|
mergePrStatuses.merge,
|
||||||
mergePrStatuses.squash,
|
mergePrStatuses.rebase,
|
||||||
mergePrStatuses.merge,
|
enablePrAutoMergeStatuses.squash,
|
||||||
mergePrStatuses.rebase,
|
enablePrAutoMergeStatuses.merge,
|
||||||
enablePrAutoMergeStatuses.squash,
|
enablePrAutoMergeStatuses.rebase,
|
||||||
enablePrAutoMergeStatuses.merge,
|
disablePrAutoMergeStatus,
|
||||||
enablePrAutoMergeStatuses.rebase,
|
mergeStatus,
|
||||||
disablePrAutoMergeStatus,
|
mergeFromBaseStatus,
|
||||||
mergeStatus,
|
archiveStatus,
|
||||||
mergeFromBaseStatus,
|
handleCommit,
|
||||||
archiveStatus,
|
handlePull,
|
||||||
handleCommit,
|
handlePush,
|
||||||
handlePull,
|
handlePullAndPush,
|
||||||
handlePush,
|
handlePrAction,
|
||||||
handlePullAndPush,
|
handleMergePr,
|
||||||
handlePrAction,
|
handleEnablePrAutoMerge,
|
||||||
handleMergePr,
|
handleDisablePrAutoMerge,
|
||||||
handleEnablePrAutoMerge,
|
handleMergeBranch,
|
||||||
handleDisablePrAutoMerge,
|
handleMergeFromBase,
|
||||||
handleMergeBranch,
|
handleArchiveWorktree,
|
||||||
handleMergeFromBase,
|
icons,
|
||||||
handleArchiveWorktree,
|
baseRef,
|
||||||
icons,
|
],
|
||||||
baseRef,
|
);
|
||||||
]);
|
|
||||||
|
const gitActions: GitActions = useMemo(
|
||||||
|
() =>
|
||||||
|
translateGitActions(buildGitActions(gitActionsInput), { baseRefLabel, hasPullRequest, t }),
|
||||||
|
[gitActionsInput, baseRefLabel, hasPullRequest, t],
|
||||||
|
);
|
||||||
|
|
||||||
return { gitActions, branchLabel, isGit };
|
return { gitActions, branchLabel, isGit };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -243,6 +243,7 @@ describe("translation resources", () => {
|
|||||||
expect(en.workspace.git.actions.commit.label).toBe("Commit");
|
expect(en.workspace.git.actions.commit.label).toBe("Commit");
|
||||||
expect(en.workspace.git.diff.binaryFile).toBe("Binary file");
|
expect(en.workspace.git.diff.binaryFile).toBe("Binary file");
|
||||||
expect(en.workspace.git.pr.sections.checks).toBe("Checks");
|
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");
|
expect(en.review.comment.placeholder).toBe("Leave a comment");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -717,6 +717,9 @@ export const ar: TranslationResources = {
|
|||||||
failedOpen: "فشل في فتح مساحة العمل",
|
failedOpen: "فشل في فتح مساحة العمل",
|
||||||
},
|
},
|
||||||
pr: {
|
pr: {
|
||||||
|
actions: {
|
||||||
|
viewPullRequest: "عرض",
|
||||||
|
},
|
||||||
sections: {
|
sections: {
|
||||||
checks: "الشيكات",
|
checks: "الشيكات",
|
||||||
reviews: "التعليقات",
|
reviews: "التعليقات",
|
||||||
|
|||||||
@@ -724,6 +724,9 @@ export const en = {
|
|||||||
failedOpen: "Failed to open workspace",
|
failedOpen: "Failed to open workspace",
|
||||||
},
|
},
|
||||||
pr: {
|
pr: {
|
||||||
|
actions: {
|
||||||
|
viewPullRequest: "View",
|
||||||
|
},
|
||||||
sections: {
|
sections: {
|
||||||
checks: "Checks",
|
checks: "Checks",
|
||||||
reviews: "Reviews",
|
reviews: "Reviews",
|
||||||
|
|||||||
@@ -744,6 +744,9 @@ export const es: TranslationResources = {
|
|||||||
failedOpen: "No se pudo abrir el espacio de trabajo",
|
failedOpen: "No se pudo abrir el espacio de trabajo",
|
||||||
},
|
},
|
||||||
pr: {
|
pr: {
|
||||||
|
actions: {
|
||||||
|
viewPullRequest: "Ver",
|
||||||
|
},
|
||||||
sections: {
|
sections: {
|
||||||
checks: "cheques",
|
checks: "cheques",
|
||||||
reviews: "Reseñas",
|
reviews: "Reseñas",
|
||||||
|
|||||||
@@ -743,6 +743,9 @@ export const fr: TranslationResources = {
|
|||||||
failedOpen: "Échec de l'ouverture de l'espace de travail",
|
failedOpen: "Échec de l'ouverture de l'espace de travail",
|
||||||
},
|
},
|
||||||
pr: {
|
pr: {
|
||||||
|
actions: {
|
||||||
|
viewPullRequest: "Voir",
|
||||||
|
},
|
||||||
sections: {
|
sections: {
|
||||||
checks: "Chèques",
|
checks: "Chèques",
|
||||||
reviews: "Avis",
|
reviews: "Avis",
|
||||||
|
|||||||
@@ -736,6 +736,9 @@ export const ru: TranslationResources = {
|
|||||||
failedOpen: "Не удалось открыть рабочую область",
|
failedOpen: "Не удалось открыть рабочую область",
|
||||||
},
|
},
|
||||||
pr: {
|
pr: {
|
||||||
|
actions: {
|
||||||
|
viewPullRequest: "Открыть",
|
||||||
|
},
|
||||||
sections: {
|
sections: {
|
||||||
checks: "Чеки",
|
checks: "Чеки",
|
||||||
reviews: "Отзывы",
|
reviews: "Отзывы",
|
||||||
|
|||||||
@@ -711,6 +711,9 @@ export const zhCN: TranslationResources = {
|
|||||||
failedOpen: "打开 workspace 失败",
|
failedOpen: "打开 workspace 失败",
|
||||||
},
|
},
|
||||||
pr: {
|
pr: {
|
||||||
|
actions: {
|
||||||
|
viewPullRequest: "查看",
|
||||||
|
},
|
||||||
sections: {
|
sections: {
|
||||||
checks: "Checks",
|
checks: "Checks",
|
||||||
reviews: "Reviews",
|
reviews: "Reviews",
|
||||||
|
|||||||
Reference in New Issue
Block a user