mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-11 02:46:29 +00:00
* 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.
158 lines
4.7 KiB
TypeScript
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,
|
|
},
|
|
}));
|