Keep Add Project method selection path-free (#3099)

* fix(app): remove search from project method selection

The fixed method list should not accept text because it can be mistaken for the directory path step that follows.

* fix(app): preserve native project picker keys

Keep hardware-keyboard delivery owned by the Add Project flow even when method selection has no visible text field.

* test(app): update add project overlay expectation
This commit is contained in:
Mohamed Boudra
2026-08-09 21:46:58 +02:00
committed by GitHub
parent ccb4e444ed
commit 02c4bc76a6
6 changed files with 89 additions and 56 deletions
@@ -13,6 +13,7 @@ import {
expectAddProjectPage,
expectNewWorkspaceForAddedProject,
openAddProjectFlow,
openAddProjectHostSelection,
} from "../support/helpers/add-project-flow";
import { gotoAppShell } from "../support/helpers/app";
import {
@@ -65,12 +66,14 @@ async function expectProjectHasNoWorkspaces(projectId: string): Promise<void> {
test.describe("Add Project command-center flow", () => {
test.describe.configure({ timeout: 180_000 });
test("a single connected host opens directly on method selection", async ({ page }) => {
test("method selection has no search field", async ({ page }) => {
await gotoAppShell(page);
await openAddProjectFlow(page);
await expect(addProjectFlowMethod(page, "directory-search")).toBeVisible();
await expect(addProjectFlowInput(page)).toHaveCount(0);
await expect(addProjectFlow(page).getByRole("textbox")).toHaveCount(0);
await expect(page.getByTestId("add-project-flow-page-host")).toHaveCount(0);
});
@@ -146,7 +149,7 @@ test.describe("Add Project command-center flow", () => {
serverId: SECONDARY_HOST_ID,
endpoint: `localhost:${secondaryHost.port}`,
});
await openAddProjectFlow(page, "host");
await openAddProjectHostSelection(page);
await page.keyboard.press("ArrowDown");
await page.keyboard.press("Enter");
@@ -155,7 +158,7 @@ test.describe("Add Project command-center flow", () => {
await expect(addProjectFlow(page)).toContainText(SECONDARY_HOST_LABEL);
});
test("Escape and Back restore page input and active selection before closing at the root", async ({
test("Escape and Back restore searchable page input before closing at the root", async ({
page,
}) => {
await gotoAppShell(page);
@@ -168,21 +171,20 @@ test.describe("Add Project command-center flow", () => {
serverId: SECONDARY_HOST_ID,
endpoint: `localhost:${secondaryHost.port}`,
});
await openAddProjectFlow(page, "host");
await openAddProjectHostSelection(page);
await addProjectFlowInput(page).fill("o");
await page.keyboard.press("ArrowDown");
await page.keyboard.press("Enter");
await expectAddProjectPage(page, "method");
await addProjectFlowInput(page).fill("new");
await page.keyboard.press("Enter");
await chooseAddProjectMethod(page, "new-directory");
await expectAddProjectPage(page, "new-directory-parent");
await page.keyboard.press("Escape");
await expectAddProjectPage(page, "method");
await expect(addProjectFlowInput(page)).toHaveValue("new");
await page.keyboard.press("Enter");
await expect(addProjectFlowInput(page)).toHaveCount(0);
await chooseAddProjectMethod(page, "new-directory");
await expectAddProjectPage(page, "new-directory-parent");
await addProjectFlowBack(page).click();
@@ -216,7 +218,7 @@ test.describe("Add Project command-center flow", () => {
serverId: SECONDARY_HOST_ID,
endpoint: `localhost:${secondaryHost.port}`,
});
await openAddProjectFlow(page, "host");
await openAddProjectHostSelection(page);
await addProjectFlowHost(page, SECONDARY_HOST_ID).click();
await expectAddProjectPage(page, "method");
@@ -160,7 +160,6 @@ test.describe("provider settings overlay stack", () => {
await commandCenter.getByText("Add project", { exact: true }).click();
const addProject = page.getByTestId("add-project-flow");
await expect(addProject).toBeVisible({ timeout: 10_000 });
await expect(addProject.getByTestId("add-project-flow-input")).toBeFocused();
await page.keyboard.press("Escape");
await expect(addProject).not.toBeVisible({ timeout: 10_000 });
await expect(selector).toBeVisible();
@@ -43,13 +43,21 @@ export async function expectAddProjectPage(page: Page, kind: AddProjectFlowPage)
return currentPage;
}
export async function openAddProjectFlow(
async function openAddProjectFlowSurface(
page: Page,
expectedPage: "host" | "method" = "method",
expectedPage: "host" | "method",
): Promise<void> {
await page.getByTestId("sidebar-add-project").click();
await expect(addProjectFlow(page)).toBeVisible({ timeout: 30_000 });
await expectAddProjectPage(page, expectedPage);
}
export async function openAddProjectFlow(page: Page): Promise<void> {
await openAddProjectFlowSurface(page, "method");
}
export async function openAddProjectHostSelection(page: Page): Promise<void> {
await openAddProjectFlowSurface(page, "host");
await expect(addProjectFlowInput(page)).toBeFocused();
}
@@ -39,9 +39,9 @@ describe("Add Project navigation", () => {
expect(currentAddProjectPage(state)).toEqual({
kind: "method",
hostId: "host-1",
query: "",
activeIndex: 0,
error: null,
isSubmitting: false,
});
expect(backAddProjectPage(state)).toBeNull();
});
+11 -5
View File
@@ -17,15 +17,18 @@ export interface GithubRepositoryChoice {
updatedAt: string | null;
}
interface SearchPageState {
query: string;
interface PageState {
activeIndex: number;
error: string | null;
}
interface SearchPageState extends PageState {
query: string;
}
export type AddProjectPage =
| ({ kind: "host" } & SearchPageState)
| ({ kind: "method"; hostId: string } & SearchPageState)
| ({ kind: "method"; hostId: string; isSubmitting: boolean } & PageState)
| ({ kind: "directory-search"; hostId: string; isSubmitting: boolean } & SearchPageState)
| ({ kind: "github-search"; hostId: string } & SearchPageState)
| ({
@@ -57,12 +60,14 @@ export interface OpenAddProjectFlowInput {
preferredHostId?: string;
}
function searchPage<TKind extends AddProjectPage["kind"]>(kind: TKind) {
type SearchPageKind = Extract<AddProjectPage, { query: string }>["kind"];
function searchPage<TKind extends SearchPageKind>(kind: TKind) {
return { kind, query: "", activeIndex: 0, error: null } as const;
}
function methodPage(hostId: string): AddProjectPage {
return { ...searchPage("method"), hostId };
return { kind: "method", hostId, activeIndex: 0, error: null, isSubmitting: false };
}
export function openAddProjectFlow(input: OpenAddProjectFlowInput): AddProjectFlowState {
@@ -209,6 +214,7 @@ export function setAddProjectPageInput(
if (current.kind === "new-directory-name") {
return { ...current, name: value, activeIndex: 0, error: null };
}
if (current.kind === "method") return current;
return { ...current, query: value, activeIndex: 0, error: null };
});
if (page.kind !== "github-location") return updated;
@@ -69,7 +69,7 @@ import {
import { Shortcut } from "@/components/ui/shortcut";
import { useKeyboardShortcutsAvailable } from "@/keyboard/availability";
import { getIsElectronRuntime } from "@/constants/layout";
import { isWeb } from "@/constants/platform";
import { isNative, isWeb } from "@/constants/platform";
import { pickDirectory } from "@/desktop/pick-directory";
import { useFetchQuery } from "@/data/query";
import { getOpenProjectFailureReason, registerProjectDescriptor } from "@/hooks/open-project";
@@ -222,12 +222,12 @@ function pageTitle(page: AddProjectPage): string {
}
}
function pagePlaceholder(page: AddProjectPage): string {
type AddProjectInputPage = Exclude<AddProjectPage, { kind: "method" }>;
function pagePlaceholder(page: AddProjectInputPage): string {
switch (page.kind) {
case "host":
return "Search hosts...";
case "method":
return "Search methods...";
case "directory-search":
return "Search directories or enter a path...";
case "github-search":
@@ -240,7 +240,7 @@ function pagePlaceholder(page: AddProjectPage): string {
}
}
function pageInput(page: AddProjectPage): string {
function pageInput(page: AddProjectInputPage): string {
return page.kind === "new-directory-name" ? page.name : page.query;
}
@@ -374,7 +374,7 @@ export function AddProjectFlow({ request, onClose }: AddProjectFlowProps) {
const inputRef = useRef<TextInput>(null);
const submissionInFlightRef = useRef(false);
const browseInFlightRef = useRef(false);
const query = page.kind === "new-directory-name" ? "" : page.query;
const query = page.kind === "new-directory-name" || page.kind === "method" ? "" : page.query;
const [debouncedQuery, setDebouncedQuery] = useState(query);
useEffect(() => {
@@ -599,23 +599,15 @@ export function AddProjectFlow({ request, onClose }: AddProjectFlowProps) {
}
if (page.kind === "method") {
if (!host) return [];
const normalized = page.query.trim().toLowerCase();
return buildAddProjectMethods(host)
.filter(
(method) =>
!normalized ||
method.label.toLowerCase().includes(normalized) ||
method.description.toLowerCase().includes(normalized),
)
.map((method) => ({
id: method.id,
title: method.label,
subtitle: method.description,
icon: methodIcon(method.id),
disabled: method.disabled,
testID: `add-project-flow-method-${method.id}`,
select: () => selectMethod(method.id),
}));
return buildAddProjectMethods(host).map((method) => ({
id: method.id,
title: method.label,
subtitle: method.description,
icon: methodIcon(method.id),
disabled: method.disabled,
testID: `add-project-flow-method-${method.id}`,
select: () => selectMethod(method.id),
}));
}
if (page.kind === "directory-search") {
return pathOptions.map((option) => {
@@ -863,21 +855,41 @@ export function AddProjectFlow({ request, onClose }: AddProjectFlowProps) {
) : null}
</View>
</View>
<ThemedTextInput
key={page.kind}
ref={inputRef}
value={pageInput(page)}
onChangeText={handleInputChange}
onKeyPress={isWeb ? undefined : handleNativeKeyPress}
onSubmitEditing={isWeb ? undefined : submitActive}
placeholder={pagePlaceholder(page)}
style={styles.input}
autoCapitalize="none"
autoCorrect={false}
editable={!isSubmitting}
returnKeyType="go"
testID="add-project-flow-input"
/>
{page.kind === "method" && isNative ? (
// Native hardware-keyboard events need a focused responder even without a visible field.
<TextInput
key={page.kind}
ref={inputRef}
onKeyPress={handleNativeKeyPress}
onSubmitEditing={submitActive}
showSoftInputOnFocus={false}
caretHidden
contextMenuHidden
accessible={false}
accessibilityElementsHidden
importantForAccessibility="no-hide-descendants"
pointerEvents="none"
style={styles.keyboardCapture}
testID="add-project-flow-keyboard-capture"
/>
) : null}
{page.kind !== "method" ? (
<ThemedTextInput
key={page.kind}
ref={inputRef}
value={pageInput(page)}
onChangeText={handleInputChange}
onKeyPress={isWeb ? undefined : handleNativeKeyPress}
onSubmitEditing={isWeb ? undefined : submitActive}
placeholder={pagePlaceholder(page)}
style={styles.input}
autoCapitalize="none"
autoCorrect={false}
editable={!isSubmitting}
returnKeyType="go"
testID="add-project-flow-input"
/>
) : null}
</View>
<ScrollView
style={styles.results}
@@ -1012,6 +1024,12 @@ const styles = StyleSheet.create((theme) => ({
paddingVertical: theme.spacing[1],
outlineStyle: "none",
} as object,
keyboardCapture: {
position: "absolute",
width: 1,
height: 1,
opacity: 0,
},
results: { flexGrow: 0, flexShrink: 1, minHeight: 0 },
resultsContent: { paddingVertical: theme.spacing[2] },
row: {