Files
paseo/packages/app/src/components/branch-switcher.tsx
T
Mohamed Boudra 616f5e155c fix(app): show branch loading and fetch errors (#8938)
* fix(app): distinguish branch loading from empty results

Preserve branch query pending and error states through the switcher so first open does not report missing branches before the response. Show request failures with a retry action.

* fix(app): keep branch retry progress and keyboard focus

Verify failed cached refresh and keyboard retry through the real browser and daemon. Keep focus in the footer while Retry is disabled so repeated Enter cannot select a cached branch.

* fix(app): preserve picker interaction when retrying branch loads

Use the existing close-and-reopen retry behavior with clear feedback rather than introducing an action that competes with native list keyboard ownership.
2026-11-06 21:35:20 +07:00

158 lines
4.7 KiB
TypeScript

import { useCallback, useMemo, useRef } from "react";
import { Text, View } from "react-native";
import { useQueryClient } from "@tanstack/react-query";
import { GitBranch } from "lucide-react-native";
import { StyleSheet, withUnistyles } from "react-native-unistyles";
import { useTranslation } from "react-i18next";
import type { Theme } from "@/styles/theme";
import { Combobox, ComboboxItem, type ComboboxProps } from "@/components/ui/combobox";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
import { useToast } from "@/contexts/toast-context";
import { useBranchSwitcher } from "@/hooks/use-branch-switcher";
import { ToolbarLabelSelectTrigger } from "@/components/ui/toolbar-label-trigger";
interface BranchSwitcherProps {
currentBranchName: string | null;
serverId: string;
workspaceId: string;
workspaceDirectory: string | null;
isGitCheckout: boolean;
testID?: string;
}
const foregroundMutedIconColorMapping = (theme: Theme) => ({
color: theme.colors.foregroundMuted,
});
const ThemedGitBranch = withUnistyles(GitBranch);
export function BranchSwitcher({
currentBranchName,
serverId,
workspaceId,
workspaceDirectory,
isGitCheckout,
testID = "workspace-header-branch-switcher",
}: BranchSwitcherProps) {
const { t } = useTranslation();
const anchorRef = useRef<View>(null);
const client = useHostRuntimeClient(serverId);
const isConnected = useHostRuntimeIsConnected(serverId);
const toast = useToast();
const queryClient = useQueryClient();
const {
branchOptions,
isLoadingBranches,
isFetchingBranches,
branchError,
isOpen,
setIsOpen,
handleBranchSelect,
} = useBranchSwitcher({
client,
normalizedServerId: serverId,
normalizedWorkspaceId: workspaceId,
workspaceDirectory,
currentBranchName,
isGitCheckout,
isConnected,
toast,
queryClient,
});
const handleOpen = useCallback(() => setIsOpen(true), [setIsOpen]);
const branchLeadingSlot = useMemo(
() => <ThemedGitBranch size={14} uniProps={foregroundMutedIconColorMapping} />,
[],
);
const renderBranchOption = useCallback<NonNullable<ComboboxProps["renderOption"]>>(
({ option, selected, active, onPress }) => (
<ComboboxItem
label={option.label}
selected={selected}
active={active}
onPress={onPress}
leadingSlot={branchLeadingSlot}
/>
),
[branchLeadingSlot],
);
let emptyText = t("branchSwitcher.empty");
if (isLoadingBranches) emptyText = t("common.states.loading");
else if (branchError) emptyText = "";
const errorFooter = useMemo(
() =>
branchError ? (
<View style={styles.footer}>
<Text accessibilityRole="alert" style={styles.error}>
{isFetchingBranches ? t("common.states.loading") : t("branchSwitcher.failedToLoad")}
</Text>
</View>
) : undefined,
[branchError, isFetchingBranches, t],
);
if (!currentBranchName) {
return null;
}
return (
<View ref={anchorRef} collapsable={false} style={styles.anchor}>
<Tooltip delayDuration={300} enabledOnDesktop enabledOnMobile={false}>
<TooltipTrigger asChild>
<ToolbarLabelSelectTrigger
testID={testID}
label={currentBranchName}
open={isOpen}
disabled={!isConnected}
onPress={handleOpen}
accessibilityRole="button"
accessibilityLabel={t("branchSwitcher.currentBranch", {
branchName: currentBranchName,
})}
/>
</TooltipTrigger>
<TooltipContent side="bottom">
<Text style={styles.tooltipText}>{t("branchSwitcher.triggerTooltip")}</Text>
</TooltipContent>
</Tooltip>
<Combobox
options={branchOptions}
value={currentBranchName}
onSelect={handleBranchSelect}
searchable
placeholder={t("branchSwitcher.placeholder")}
searchPlaceholder={t("branchSwitcher.searchPlaceholder")}
emptyText={emptyText}
footer={errorFooter}
title={t("branchSwitcher.title")}
open={isOpen}
onOpenChange={setIsOpen}
anchorRef={anchorRef}
desktopPlacement="bottom-start"
desktopPreventInitialFlash
desktopMinWidth={280}
renderOption={renderBranchOption}
/>
</View>
);
}
const styles = StyleSheet.create((theme) => ({
anchor: {
flexShrink: 1,
minWidth: 0,
},
footer: { padding: theme.spacing[3], gap: theme.spacing[2] },
error: { color: theme.colors.destructive, fontSize: theme.fontSize.sm },
tooltipText: {
color: theme.colors.popoverForeground,
fontSize: theme.fontSize.sm,
},
}));