mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
fix(app): clean up host selection affordances
Single-host users should not see host-selection chrome, while multi-host surfaces keep shared combobox feedback and consistent hover-card metadata rows.
This commit is contained in:
@@ -74,6 +74,37 @@ test.describe("New workspace entry points", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("the New Workspace screen hides the host selector when there is only one host", async ({
|
||||
page,
|
||||
}) => {
|
||||
const seeded: SeededWorkspace = await seedWorkspace({ repoPrefix: "entry-single-host-" });
|
||||
|
||||
try {
|
||||
await seedSavedSettingsHosts(page, [
|
||||
{
|
||||
serverId: getServerId(),
|
||||
label: "localhost",
|
||||
endpoint: `127.0.0.1:${getE2EDaemonPort()}`,
|
||||
},
|
||||
]);
|
||||
|
||||
await gotoAppShell(page);
|
||||
await waitForSidebarHydration(page);
|
||||
await expect(
|
||||
page.getByTestId(`sidebar-workspace-row-${getServerId()}:${seeded.workspaceId}`),
|
||||
).toBeVisible({ timeout: 30_000 });
|
||||
|
||||
await openGlobalNewWorkspaceComposer(page);
|
||||
|
||||
await expect(page.getByTestId("new-workspace-project-picker-trigger")).toBeVisible({
|
||||
timeout: 30_000,
|
||||
});
|
||||
await expect(page.getByTestId("host-picker-trigger")).toHaveCount(0);
|
||||
} finally {
|
||||
await seeded.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test("each project's row icon preselects that project, and the reused screen resets a stale manual choice across projects", async ({
|
||||
page,
|
||||
}) => {
|
||||
|
||||
@@ -132,6 +132,25 @@ test.describe("Sidebar workspace list", () => {
|
||||
await workspace.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test("workspace hover card shows host as metadata", async ({ page }) => {
|
||||
const workspace = await seedWorkspace({ repoPrefix: "sidebar-hover-host-" });
|
||||
|
||||
try {
|
||||
await gotoAppShell(page);
|
||||
await waitForSidebarProject(page, path.basename(workspace.repoPath));
|
||||
|
||||
const row = await waitForSidebarWorkspace(page, workspace.workspaceId);
|
||||
await row.hover();
|
||||
|
||||
const hoverCard = page.getByTestId("workspace-hover-card");
|
||||
await expect(hoverCard).toBeVisible({ timeout: 30_000 });
|
||||
await expect(page.getByTestId("hover-card-workspace-host")).toHaveText("localhost");
|
||||
await expect(hoverCard).not.toContainText(/\b(Online|Connecting|Offline|Error|Idle)\b/);
|
||||
} finally {
|
||||
await workspace.cleanup();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Mobile sidebar panelState transition", () => {
|
||||
|
||||
@@ -100,7 +100,6 @@ export function HostPickerOption({
|
||||
trailingSlot={trailingSlot}
|
||||
selected={selected}
|
||||
active={active}
|
||||
interactiveFeedback={false}
|
||||
onPress={onPress}
|
||||
testID={testID}
|
||||
/>
|
||||
@@ -134,7 +133,6 @@ function SystemHostPickerOption({
|
||||
leadingSlot={leadingSlot}
|
||||
selected={selected}
|
||||
active={active}
|
||||
interactiveFeedback={false}
|
||||
onPress={onPress}
|
||||
testID={testID}
|
||||
/>
|
||||
|
||||
@@ -230,7 +230,6 @@ export interface ComboboxItemProps {
|
||||
disabled?: boolean;
|
||||
/** When true, bumps hover/pressed colors up one surface level (for items on elevated backgrounds). */
|
||||
elevated?: boolean;
|
||||
interactiveFeedback?: boolean;
|
||||
onPress: () => void;
|
||||
testID?: string;
|
||||
}
|
||||
@@ -245,7 +244,6 @@ export function ComboboxItem({
|
||||
active,
|
||||
disabled,
|
||||
elevated,
|
||||
interactiveFeedback = true,
|
||||
onPress,
|
||||
testID,
|
||||
}: ComboboxItemProps): ReactElement {
|
||||
@@ -271,16 +269,12 @@ export function ComboboxItem({
|
||||
const itemPressableStyle = useCallback(
|
||||
({ pressed, hovered = false }: PressableStateCallbackType & { hovered?: boolean }) => [
|
||||
styles.comboboxItem,
|
||||
interactiveFeedback &&
|
||||
hovered &&
|
||||
(elevated ? styles.comboboxItemHoveredElevated : styles.comboboxItemHovered),
|
||||
interactiveFeedback &&
|
||||
pressed &&
|
||||
(elevated ? styles.comboboxItemPressedElevated : styles.comboboxItemPressed),
|
||||
interactiveFeedback && active && styles.comboboxItemActive,
|
||||
hovered && (elevated ? styles.comboboxItemHoveredElevated : styles.comboboxItemHovered),
|
||||
pressed && (elevated ? styles.comboboxItemPressedElevated : styles.comboboxItemPressed),
|
||||
active && styles.comboboxItemActive,
|
||||
disabled && styles.comboboxItemDisabled,
|
||||
],
|
||||
[elevated, active, disabled, interactiveFeedback],
|
||||
[elevated, active, disabled],
|
||||
);
|
||||
|
||||
const itemContentStyle = useMemo(
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
ExternalLink,
|
||||
Folder,
|
||||
GitBranch,
|
||||
Server,
|
||||
} from "lucide-react-native";
|
||||
import { GitHubIcon } from "@/components/icons/github-icon";
|
||||
import type { Theme } from "@/styles/theme";
|
||||
@@ -39,8 +40,7 @@ import { useHoverSafeZone } from "@/hooks/use-hover-safe-zone";
|
||||
import { useIsCompactFormFactor } from "@/constants/layout";
|
||||
import { FloatingSurface } from "@/components/ui/floating";
|
||||
import { isWeb } from "@/constants/platform";
|
||||
import { useHosts, useHostRuntimeSnapshot } from "@/runtime/host-runtime";
|
||||
import { formatConnectionStatus } from "@/utils/daemons";
|
||||
import { useHosts } from "@/runtime/host-runtime";
|
||||
|
||||
interface Rect {
|
||||
x: number;
|
||||
@@ -330,20 +330,16 @@ function WorkspaceHoverCardContent({
|
||||
|
||||
const ThemedGitBranch = withUnistyles(GitBranch);
|
||||
const ThemedFolder = withUnistyles(Folder);
|
||||
const ThemedServer = withUnistyles(Server);
|
||||
|
||||
type CardInfoIcon = React.ComponentType<React.ComponentProps<typeof ThemedGitBranch>>;
|
||||
|
||||
function HostRow({ serverId }: { serverId: string }): ReactElement | null {
|
||||
const hosts = useHosts();
|
||||
const host = hosts.find((h) => h.serverId === serverId);
|
||||
const snapshot = useHostRuntimeSnapshot(serverId);
|
||||
const label = host?.label?.trim() || serverId;
|
||||
const status = formatConnectionStatus(snapshot?.connectionStatus ?? "idle");
|
||||
|
||||
return (
|
||||
<View style={styles.hostRow}>
|
||||
<Text style={styles.hostRowLabel}>{label}</Text>
|
||||
<Text style={styles.hostRowStatus}>{status}</Text>
|
||||
</View>
|
||||
);
|
||||
return <InfoRow icon={ThemedServer} value={label} testID="hover-card-workspace-host" />;
|
||||
}
|
||||
|
||||
const ThemedExternalLink = withUnistyles(ExternalLink);
|
||||
@@ -360,6 +356,25 @@ const successColorMapping = (theme: Theme) => ({ color: theme.colors.statusSucce
|
||||
const warningColorMapping = (theme: Theme) => ({ color: theme.colors.statusWarning });
|
||||
const dangerColorMapping = (theme: Theme) => ({ color: theme.colors.statusDanger });
|
||||
|
||||
function InfoRow({
|
||||
icon: Icon,
|
||||
value,
|
||||
testID,
|
||||
}: {
|
||||
icon: CardInfoIcon;
|
||||
value: string;
|
||||
testID: string;
|
||||
}) {
|
||||
return (
|
||||
<View style={styles.cardInfoRow}>
|
||||
<Icon size={12} uniProps={foregroundMutedColorMapping} />
|
||||
<Text style={styles.cardInfoText} numberOfLines={1} testID={testID}>
|
||||
{value}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function CopyableInfoRow({
|
||||
icon: Icon,
|
||||
value,
|
||||
@@ -367,7 +382,7 @@ function CopyableInfoRow({
|
||||
copyLabel,
|
||||
testID,
|
||||
}: {
|
||||
icon: React.ComponentType<React.ComponentProps<typeof ThemedGitBranch>>;
|
||||
icon: CardInfoIcon;
|
||||
value: string;
|
||||
copyValue: string;
|
||||
copyLabel: string;
|
||||
@@ -567,22 +582,6 @@ const styles = StyleSheet.create((theme) => ({
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
},
|
||||
hostRow: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
gap: theme.spacing[2],
|
||||
paddingHorizontal: theme.spacing[3],
|
||||
paddingBottom: theme.spacing[2],
|
||||
},
|
||||
hostRowLabel: {
|
||||
fontSize: theme.fontSize.xs,
|
||||
color: theme.colors.foregroundMuted,
|
||||
},
|
||||
hostRowStatus: {
|
||||
fontSize: theme.fontSize.xs,
|
||||
color: theme.colors.foregroundMuted,
|
||||
},
|
||||
cardMetaRow: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
|
||||
@@ -1196,6 +1196,7 @@ function useNewWorkspaceFormStack(input: NewWorkspaceFormStackInput): ReactEleme
|
||||
|
||||
const selectedHostLabel =
|
||||
host.allHosts.find((h) => h.serverId === host.selectedServerId)?.label ?? "Host";
|
||||
const showHostControl = host.allHosts.length > 1;
|
||||
const isolationTriggerLabel = isolationLabel(t, isolation.effectiveIsolation);
|
||||
|
||||
const badgePressableStyle = useCallback(
|
||||
@@ -1242,7 +1243,7 @@ function useNewWorkspaceFormStack(input: NewWorkspaceFormStackInput): ReactEleme
|
||||
</View>
|
||||
);
|
||||
|
||||
const hostControl = (
|
||||
const hostControl = showHostControl ? (
|
||||
<View>
|
||||
<HostPicker
|
||||
hosts={host.allHosts}
|
||||
@@ -1271,7 +1272,7 @@ function useNewWorkspaceFormStack(input: NewWorkspaceFormStackInput): ReactEleme
|
||||
</Pressable>
|
||||
</HostPicker>
|
||||
</View>
|
||||
);
|
||||
) : null;
|
||||
|
||||
const isolationControl = isolation.canCreateWorktree ? (
|
||||
<View>
|
||||
@@ -1334,7 +1335,7 @@ function useNewWorkspaceFormStack(input: NewWorkspaceFormStackInput): ReactEleme
|
||||
return isCompact ? (
|
||||
<View testID="new-workspace-ref-picker-row" style={styles.formStack}>
|
||||
<FormRow>{projectControl}</FormRow>
|
||||
<FormRow>{hostControl}</FormRow>
|
||||
{hostControl ? <FormRow>{hostControl}</FormRow> : null}
|
||||
{/* Keep fixed row height when git-only controls are hidden. */}
|
||||
{isolationControl ? (
|
||||
<FormRow>{isolationControl}</FormRow>
|
||||
|
||||
Reference in New Issue
Block a user