From fb4ca20dbd06ab298fa01ce9d65078699cbfbc80 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Tue, 20 Oct 2026 07:18:09 +0200 Subject: [PATCH] fix(hosts): keep compact row actions clear of host labels Give compact connected-host rows separate text and action widths so long names and producer labels cannot compete with primary controls. --- .../app/e2e/browser/connected-hosts.spec.ts | 33 +++++++++++++++++++ .../e2e/support/helpers/connected-hosts.ts | 15 +++++++++ .../app/src/components/settings/index.tsx | 16 +++++++-- packages/app/src/connected-hosts/index.tsx | 2 ++ 4 files changed, 63 insertions(+), 3 deletions(-) diff --git a/packages/app/e2e/browser/connected-hosts.spec.ts b/packages/app/e2e/browser/connected-hosts.spec.ts index 98a25e150a..bb86b7b949 100644 --- a/packages/app/e2e/browser/connected-hosts.spec.ts +++ b/packages/app/e2e/browser/connected-hosts.spec.ts @@ -1,5 +1,8 @@ import { test } from "../support/fixtures"; import { + expectConnectedHostControlsFit, + closeConnectedHostDetails, + toggleConnectedHost, openRestartableConnectedHosts, beginConnectedHostDraft, reconnectManagingHost, @@ -64,3 +67,33 @@ test("connection loss retains the unsaved name and credential input", async ({ p await host.close(); } }); + +import { startIsolatedHostDaemon } from "../support/helpers/isolated-host-daemon"; + +test.describe("compact connected host controls", () => { + test.use({ viewport: { width: 390, height: 844 } }); + test("long host names keep both actions visible with details open and closed", async ({ + page, + }, testInfo) => { + const destination = await startIsolatedHostDaemon("srv_connected_layout"); + const label = "Production build host — development-infrastructure-europe-west"; + try { + await openCompactConnectedHosts(page); + await addConnectedHost(page, `127.0.0.1:${destination.port}`, label); + await expectConnectedHostControlsFit(page, label, true); + await inspectConnectedHost(page, label); + await expectConnectedHostControlsFit(page, label, true); + await closeConnectedHostDetails(page); + await toggleConnectedHost(page, true); + await expectConnectedHostControlsFit(page, label, false); + await inspectConnectedHost(page, label); + await expectConnectedHostControlsFit(page, label, false); + await page.screenshot({ path: testInfo.outputPath("connected-host-controls.png") }); + await closeConnectedHostDetails(page); + await toggleConnectedHost(page, false); + await expectConnectedHostControlsFit(page, label, true); + } finally { + await destination.close(); + } + }); +}); diff --git a/packages/app/e2e/support/helpers/connected-hosts.ts b/packages/app/e2e/support/helpers/connected-hosts.ts index 70c288e4c4..a608141418 100644 --- a/packages/app/e2e/support/helpers/connected-hosts.ts +++ b/packages/app/e2e/support/helpers/connected-hosts.ts @@ -115,3 +115,18 @@ export async function editConnectedHost(page: Page) { page.getByRole("textbox", { name: "Replacement connection", exact: true }), ).toHaveValue(""); } + +export async function expectConnectedHostControlsFit(page: Page, label: string, enabled: boolean) { + const details = page.getByRole("button", { name: `Details for ${label}`, exact: true }); + const toggle = page.getByRole("button", { name: enabled ? "Disable" : "Enable", exact: true }); + await details.scrollIntoViewIfNeeded(); + await expect(details).toBeInViewport({ ratio: 1 }); + await expect(toggle).toBeInViewport({ ratio: 1 }); +} +export async function closeConnectedHostDetails(page: Page) { + await page.getByRole("button", { name: "Cancel", exact: true }).click(); +} +export async function toggleConnectedHost(page: Page, enabled: boolean) { + await page.getByRole("button", { name: enabled ? "Disable" : "Enable", exact: true }).click(); + await expect(page.getByText(enabled ? /disabled · Disabled/ : /online · Enabled/)).toBeVisible(); +} diff --git a/packages/app/src/components/settings/index.tsx b/packages/app/src/components/settings/index.tsx index f31a0e7b38..2bc32f9dcf 100644 --- a/packages/app/src/components/settings/index.tsx +++ b/packages/app/src/components/settings/index.tsx @@ -31,6 +31,7 @@ export { SettingsCollapsibleRow } from "./collapsible-row"; interface AppSettingsRowProps extends Omit { hint?: ReactNode; labelAccessory?: ReactNode; + stacked?: boolean; } export function SettingsCard({ children, testID }: { children: ReactNode; testID?: string }) { @@ -55,12 +56,16 @@ export function SettingsRow({ error, children, testID, + stacked = false, }: AppSettingsRowProps) { const compact = useIsCompactFormFactor(); - const rowStyle = useMemo(() => [settingsStyles.row, compact && styles.compactRow], [compact]); + const rowStyle = useMemo( + () => [settingsStyles.row, compact && styles.compactRow, stacked && styles.stackedRow], + [compact, stacked], + ); return ( - + {labelAccessory ? ( {label} @@ -76,7 +81,9 @@ export function SettingsRow({ ) : null} - {children ? {children} : null} + {children ? ( + {children} + ) : null} ); } @@ -193,6 +200,9 @@ export function SettingsAction({ actionLabel, onPress, disabled, ...row }: Setti } const styles = StyleSheet.create((theme) => ({ + stackedRow: { flexDirection: "column", alignItems: "stretch", gap: theme.spacing[3] }, + stackedLabel: { flexBasis: "auto", marginRight: 0, minWidth: 0 }, + stackedControl: { width: "100%" }, compactRow: { flexWrap: "wrap", gap: theme.spacing[3] }, label: { flexGrow: 1, flexShrink: 1, flexBasis: 160, marginRight: theme.spacing[3] }, labelRow: { diff --git a/packages/app/src/connected-hosts/index.tsx b/packages/app/src/connected-hosts/index.tsx index e1966f1508..5ba25afdd7 100644 --- a/packages/app/src/connected-hosts/index.tsx +++ b/packages/app/src/connected-hosts/index.tsx @@ -146,10 +146,12 @@ function HostRow({ canManage: boolean; pending: boolean; }) { + const compact = useIsCompactFormFactor(); const edit = useCallback(() => model.edit(host), [host, model]); const toggle = useCallback(() => void model.setEnabled(host), [host, model]); return (