Always show workspace hover card with branch and directory
The hover card was previously gated to open only when a PR hint or diff stat was available. It now opens on every desktop-web workspace hover so users can see the workspace name, branch, and working directory. The CWD is shortened using the existing shortenPath helper (/Users/... -> ~).
This commit is contained in:
@@ -12,7 +12,14 @@ import { Dimensions, Text, View } from "react-native";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { FadeIn, FadeOut } from "react-native-reanimated";
|
import { FadeIn, FadeOut } from "react-native-reanimated";
|
||||||
import { StyleSheet, withUnistyles } from "react-native-unistyles";
|
import { StyleSheet, withUnistyles } from "react-native-unistyles";
|
||||||
import { CircleCheck, CircleDot, CircleX, ExternalLink, GitBranch } from "lucide-react-native";
|
import {
|
||||||
|
CircleCheck,
|
||||||
|
CircleDot,
|
||||||
|
CircleX,
|
||||||
|
ExternalLink,
|
||||||
|
Folder,
|
||||||
|
GitBranch,
|
||||||
|
} from "lucide-react-native";
|
||||||
import { GitHubIcon } from "@/components/icons/github-icon";
|
import { GitHubIcon } from "@/components/icons/github-icon";
|
||||||
import type { Theme } from "@/styles/theme";
|
import type { Theme } from "@/styles/theme";
|
||||||
import { DiffStat } from "@/components/diff-stat";
|
import { DiffStat } from "@/components/diff-stat";
|
||||||
@@ -22,6 +29,7 @@ import { useBottomSheetModalInternal } from "@gorhom/bottom-sheet";
|
|||||||
import type { SidebarWorkspaceEntry } from "@/hooks/use-sidebar-workspaces-list";
|
import type { SidebarWorkspaceEntry } from "@/hooks/use-sidebar-workspaces-list";
|
||||||
import type { PrHint } from "@/git/use-pr-status-query";
|
import type { PrHint } from "@/git/use-pr-status-query";
|
||||||
import { openExternalUrl } from "@/utils/open-external-url";
|
import { openExternalUrl } from "@/utils/open-external-url";
|
||||||
|
import { shortenPath } from "@/utils/shorten-path";
|
||||||
import { PrBadge } from "@/components/sidebar-workspace-list";
|
import { PrBadge } from "@/components/sidebar-workspace-list";
|
||||||
import { useHoverSafeZone } from "@/hooks/use-hover-safe-zone";
|
import { useHoverSafeZone } from "@/hooks/use-hover-safe-zone";
|
||||||
import { useIsCompactFormFactor } from "@/constants/layout";
|
import { useIsCompactFormFactor } from "@/constants/layout";
|
||||||
@@ -113,8 +121,6 @@ function WorkspaceHoverCardDesktop({
|
|||||||
const graceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const graceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
const triggerHoveredRef = useRef(false);
|
const triggerHoveredRef = useRef(false);
|
||||||
|
|
||||||
const hasContent = prHint !== null || !!workspace.diffStat;
|
|
||||||
|
|
||||||
const clearGraceTimer = useCallback(() => {
|
const clearGraceTimer = useCallback(() => {
|
||||||
if (graceTimerRef.current) {
|
if (graceTimerRef.current) {
|
||||||
clearTimeout(graceTimerRef.current);
|
clearTimeout(graceTimerRef.current);
|
||||||
@@ -133,10 +139,10 @@ function WorkspaceHoverCardDesktop({
|
|||||||
const handleTriggerEnter = useCallback(() => {
|
const handleTriggerEnter = useCallback(() => {
|
||||||
triggerHoveredRef.current = true;
|
triggerHoveredRef.current = true;
|
||||||
clearGraceTimer();
|
clearGraceTimer();
|
||||||
if (!isDragging && hasContent) {
|
if (!isDragging) {
|
||||||
setOpen(true);
|
setOpen(true);
|
||||||
}
|
}
|
||||||
}, [clearGraceTimer, isDragging, hasContent]);
|
}, [clearGraceTimer, isDragging]);
|
||||||
|
|
||||||
const handleTriggerLeave = useCallback(() => {
|
const handleTriggerLeave = useCallback(() => {
|
||||||
triggerHoveredRef.current = false;
|
triggerHoveredRef.current = false;
|
||||||
@@ -162,19 +168,6 @@ function WorkspaceHoverCardDesktop({
|
|||||||
}
|
}
|
||||||
}, [isDragging, clearGraceTimer]);
|
}, [isDragging, clearGraceTimer]);
|
||||||
|
|
||||||
// When content becomes available while trigger is already hovered, open the card.
|
|
||||||
useEffect(() => {
|
|
||||||
if (!hasContent) {
|
|
||||||
clearGraceTimer();
|
|
||||||
setOpen(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isDragging) return;
|
|
||||||
if (triggerHoveredRef.current) {
|
|
||||||
setOpen(true);
|
|
||||||
}
|
|
||||||
}, [clearGraceTimer, hasContent, isDragging]);
|
|
||||||
|
|
||||||
// Cleanup on unmount
|
// Cleanup on unmount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
@@ -190,7 +183,7 @@ function WorkspaceHoverCardDesktop({
|
|||||||
onPointerLeave={handleTriggerLeave}
|
onPointerLeave={handleTriggerLeave}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
{open && hasContent ? (
|
{open ? (
|
||||||
<WorkspaceHoverCardContent
|
<WorkspaceHoverCardContent
|
||||||
workspace={workspace}
|
workspace={workspace}
|
||||||
prHint={prHint}
|
prHint={prHint}
|
||||||
@@ -214,6 +207,7 @@ function WorkspaceHoverCardContent({
|
|||||||
contentRef: React.RefObject<View | null>;
|
contentRef: React.RefObject<View | null>;
|
||||||
}): ReactElement | null {
|
}): ReactElement | null {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const cwdDisplay = shortenPath(workspace.workspaceDirectory);
|
||||||
const bottomSheetInternal = useBottomSheetModalInternal(true);
|
const bottomSheetInternal = useBottomSheetModalInternal(true);
|
||||||
const [triggerRect, setTriggerRect] = useState<Rect | null>(null);
|
const [triggerRect, setTriggerRect] = useState<Rect | null>(null);
|
||||||
const [contentSize, setContentSize] = useState<{ width: number; height: number } | null>(null);
|
const [contentSize, setContentSize] = useState<{ width: number; height: number } | null>(null);
|
||||||
@@ -286,11 +280,11 @@ function WorkspaceHoverCardContent({
|
|||||||
{workspace.name}
|
{workspace.name}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
{workspace.currentBranch && workspace.currentBranch !== workspace.name ? (
|
{workspace.currentBranch ? (
|
||||||
<View style={styles.cardBranchRow}>
|
<View style={styles.cardInfoRow}>
|
||||||
<ThemedGitBranch size={12} uniProps={foregroundMutedColorMapping} />
|
<ThemedGitBranch size={12} uniProps={foregroundMutedColorMapping} />
|
||||||
<Text
|
<Text
|
||||||
style={styles.cardBranchText}
|
style={styles.cardInfoText}
|
||||||
numberOfLines={1}
|
numberOfLines={1}
|
||||||
testID="hover-card-workspace-branch"
|
testID="hover-card-workspace-branch"
|
||||||
>
|
>
|
||||||
@@ -298,6 +292,14 @@ function WorkspaceHoverCardContent({
|
|||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
|
{cwdDisplay ? (
|
||||||
|
<View style={styles.cardInfoRow}>
|
||||||
|
<ThemedFolder size={12} uniProps={foregroundMutedColorMapping} />
|
||||||
|
<Text style={styles.cardInfoText} numberOfLines={1} testID="hover-card-workspace-cwd">
|
||||||
|
{cwdDisplay}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
{prHint || workspace.diffStat ? (
|
{prHint || workspace.diffStat ? (
|
||||||
<View style={styles.cardMetaRow}>
|
<View style={styles.cardMetaRow}>
|
||||||
{workspace.diffStat ? (
|
{workspace.diffStat ? (
|
||||||
@@ -322,6 +324,7 @@ function WorkspaceHoverCardContent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ThemedGitBranch = withUnistyles(GitBranch);
|
const ThemedGitBranch = withUnistyles(GitBranch);
|
||||||
|
const ThemedFolder = withUnistyles(Folder);
|
||||||
const ThemedExternalLink = withUnistyles(ExternalLink);
|
const ThemedExternalLink = withUnistyles(ExternalLink);
|
||||||
const ThemedGitHubIcon = withUnistyles(GitHubIcon);
|
const ThemedGitHubIcon = withUnistyles(GitHubIcon);
|
||||||
const ThemedCircleCheck = withUnistyles(CircleCheck);
|
const ThemedCircleCheck = withUnistyles(CircleCheck);
|
||||||
@@ -484,14 +487,14 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
paddingHorizontal: theme.spacing[3],
|
paddingHorizontal: theme.spacing[3],
|
||||||
paddingBottom: theme.spacing[2],
|
paddingBottom: theme.spacing[2],
|
||||||
},
|
},
|
||||||
cardBranchRow: {
|
cardInfoRow: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: theme.spacing[1.5],
|
gap: theme.spacing[1.5],
|
||||||
paddingHorizontal: theme.spacing[3],
|
paddingHorizontal: theme.spacing[3],
|
||||||
paddingBottom: theme.spacing[2],
|
paddingBottom: theme.spacing[2],
|
||||||
},
|
},
|
||||||
cardBranchText: {
|
cardInfoText: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
color: theme.colors.foregroundMuted,
|
color: theme.colors.foregroundMuted,
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { shortenPath } from "./shorten-path";
|
||||||
|
|
||||||
|
describe("shortenPath", () => {
|
||||||
|
it("shortens a macOS home directory path", () => {
|
||||||
|
expect(shortenPath("/Users/moboudra/dev/paseo")).toBe("~/dev/paseo");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shortens a Linux home directory path", () => {
|
||||||
|
expect(shortenPath("/home/moboudra/dev/paseo")).toBe("~/dev/paseo");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("leaves non-home absolute paths unchanged", () => {
|
||||||
|
expect(shortenPath("/var/www/app")).toBe("/var/www/app");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("leaves Windows paths unchanged", () => {
|
||||||
|
expect(shortenPath("C:\\Users\\moboudra\\dev\\paseo")).toBe("C:\\Users\\moboudra\\dev\\paseo");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns an empty string for null or undefined", () => {
|
||||||
|
expect(shortenPath(null)).toBe("");
|
||||||
|
expect(shortenPath(undefined)).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns an empty string for an empty string", () => {
|
||||||
|
expect(shortenPath("")).toBe("");
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user