diff --git a/packages/app/e2e/empty-project-persists.spec.ts b/packages/app/e2e/empty-project-persists.spec.ts index e7c37b914..30102e197 100644 --- a/packages/app/e2e/empty-project-persists.spec.ts +++ b/packages/app/e2e/empty-project-persists.spec.ts @@ -73,6 +73,23 @@ async function waitForSidebarProjectListReady(page: Page): Promise { .waitFor({ state: "visible", timeout: 60_000 }); } +test.describe("Project picker search", () => { + test("shows a loading state after typing while directory suggestions are pending", async ({ + page, + }) => { + await gotoAppShell(page); + await waitForSidebarProjectListReady(page); + await page.getByTestId("sidebar-add-project").click(); + + const input = page.getByPlaceholder("Type a directory path..."); + await expect(input).toBeVisible({ timeout: 30_000 }); + await input.fill("paseo-loading-state-no-match"); + + await expect(page.getByText("Start typing a path", { exact: true })).toHaveCount(0); + await expect(page.getByText("Searching...", { exact: true })).toBeVisible(); + }); +}); + // Projects are parents in the sidebar. Archiving the last workspace leaves the // project row in place with a ghost "+ New workspace" child row. test.describe("Project with no workspaces persists", () => { diff --git a/packages/app/src/components/project-picker-modal.tsx b/packages/app/src/components/project-picker-modal.tsx index 367436200..cfa32afe0 100644 --- a/packages/app/src/components/project-picker-modal.tsx +++ b/packages/app/src/components/project-picker-modal.tsx @@ -7,6 +7,8 @@ import { TextInput, View, type PressableStateCallbackType, + type StyleProp, + type TextStyle, } from "react-native"; import { useQuery } from "@tanstack/react-query"; import { useTranslation } from "react-i18next"; @@ -68,6 +70,68 @@ function PathRow({ option, active, onSelect }: PathRowProps) { ); } +interface ProjectPickerResultsProps { + options: ProjectPickerOption[]; + activeIndex: number; + isSubmitting: boolean; + openErrorMessage: string | null; + hasQuery: boolean; + isSearching: boolean; + emptyTextStyle: StyleProp; + errorTextStyle: StyleProp; + onSelect: (path: string) => void; +} + +function ProjectPickerResults({ + options, + activeIndex, + isSubmitting, + openErrorMessage, + hasQuery, + isSearching, + emptyTextStyle, + errorTextStyle, + onSelect, +}: ProjectPickerResultsProps) { + const { t } = useTranslation(); + const canShowResultState = !isSubmitting && !openErrorMessage; + + return ( + + {isSubmitting ? {t("projectPicker.opening")} : null} + {!isSubmitting && openErrorMessage ? ( + {openErrorMessage} + ) : null} + {canShowResultState && options.length === 0 && !hasQuery ? ( + {t("projectPicker.empty")} + ) : null} + {canShowResultState && isSearching ? ( + {t("projectPicker.searching")} + ) : null} + {canShowResultState && !isSearching && options.length === 0 && hasQuery ? ( + {t("common.empty.noOptionsMatchSearch")} + ) : null} + {canShowResultState && options.length > 0 ? ( + <> + {options.map((option, index) => ( + + ))} + + ) : null} + + ); +} + export function ProjectPickerModal() { const { theme } = useUnistyles(); const { t } = useTranslation(); @@ -116,6 +180,11 @@ export function ProjectPickerModal() { }), [directorySuggestionsQuery.data, query, recommendedPaths], ); + const hasQuery = query.trim().length > 0; + const isSearching = + hasQuery && + options.length === 0 && + (query !== debouncedQuery || directorySuggestionsQuery.isFetching); const openErrorMessage = useMemo(() => { if (!openErrorReason) { @@ -276,32 +345,17 @@ export function ProjectPickerModal() { /> - - {isSubmitting ? {t("projectPicker.opening")} : null} - {!isSubmitting && openErrorMessage ? ( - {openErrorMessage} - ) : null} - {!isSubmitting && options.length === 0 && !query.trim() ? ( - {t("projectPicker.empty")} - ) : null} - {!isSubmitting && !(options.length === 0 && !query.trim()) ? ( - <> - {options.map((option, index) => ( - - ))} - - ) : null} - + diff --git a/packages/app/src/i18n/resources.test.ts b/packages/app/src/i18n/resources.test.ts index bc4b48f92..7d4b1b0dc 100644 --- a/packages/app/src/i18n/resources.test.ts +++ b/packages/app/src/i18n/resources.test.ts @@ -388,6 +388,7 @@ describe("translation resources", () => { it("includes picker, file pane, and tool detail keys for the Batch 4L migration", () => { expect(en.projectPicker.placeholder).toBe("Type a directory path..."); expect(en.projectPicker.opening).toBe("Opening project..."); + expect(en.projectPicker.searching).toBe("Searching..."); expect(en.projectPicker.empty).toBe("Start typing a path"); expect(en.branchSwitcher.currentBranch).toBe( "Current branch: {{branchName}}. Press to switch branch.", diff --git a/packages/app/src/i18n/resources/ar.ts b/packages/app/src/i18n/resources/ar.ts index 2319a64f6..24bf54b36 100644 --- a/packages/app/src/i18n/resources/ar.ts +++ b/packages/app/src/i18n/resources/ar.ts @@ -1054,6 +1054,7 @@ export const ar: TranslationResources = { projectPicker: { placeholder: "اكتب مسار الدليل...", opening: "افتتاح المشروع...", + searching: "جارٍ البحث...", empty: "ابدأ بكتابة المسار", errors: { directory_not_found: "لم يتم العثور على الدليل.", diff --git a/packages/app/src/i18n/resources/en.ts b/packages/app/src/i18n/resources/en.ts index 18125001b..35a12a7ff 100644 --- a/packages/app/src/i18n/resources/en.ts +++ b/packages/app/src/i18n/resources/en.ts @@ -1060,6 +1060,7 @@ export const en = { projectPicker: { placeholder: "Type a directory path...", opening: "Opening project...", + searching: "Searching...", empty: "Start typing a path", errors: { directory_not_found: "Directory not found.", diff --git a/packages/app/src/i18n/resources/es.ts b/packages/app/src/i18n/resources/es.ts index c40f29c43..2b2f0fbbe 100644 --- a/packages/app/src/i18n/resources/es.ts +++ b/packages/app/src/i18n/resources/es.ts @@ -1087,6 +1087,7 @@ export const es: TranslationResources = { projectPicker: { placeholder: "Escriba una ruta de directorio...", opening: "Proyecto de apertura...", + searching: "Buscando...", empty: "Comience a escribir una ruta", errors: { directory_not_found: "No se encontró el directorio.", diff --git a/packages/app/src/i18n/resources/fr.ts b/packages/app/src/i18n/resources/fr.ts index 1217f752a..5c8a33779 100644 --- a/packages/app/src/i18n/resources/fr.ts +++ b/packages/app/src/i18n/resources/fr.ts @@ -1089,6 +1089,7 @@ export const fr: TranslationResources = { projectPicker: { placeholder: "Tapez un chemin de répertoire...", opening: "Projet d'ouverture...", + searching: "Recherche en cours...", empty: "Commencez à taper un chemin", errors: { directory_not_found: "Répertoire introuvable.", diff --git a/packages/app/src/i18n/resources/ja.ts b/packages/app/src/i18n/resources/ja.ts index 7c2f520e3..4dc29ae4e 100644 --- a/packages/app/src/i18n/resources/ja.ts +++ b/packages/app/src/i18n/resources/ja.ts @@ -1067,6 +1067,7 @@ export const ja: TranslationResources = { projectPicker: { placeholder: "ディレクトリパスを入力...", opening: "プロジェクトを開いています...", + searching: "検索中...", empty: "パスを入力してください", errors: { directory_not_found: "ディレクトリが見つかりません。", diff --git a/packages/app/src/i18n/resources/pt-BR.ts b/packages/app/src/i18n/resources/pt-BR.ts index a4613aecb..3dcad94fa 100644 --- a/packages/app/src/i18n/resources/pt-BR.ts +++ b/packages/app/src/i18n/resources/pt-BR.ts @@ -1073,6 +1073,7 @@ export const ptBR: TranslationResources = { projectPicker: { placeholder: "Digite um caminho de diretório...", opening: "Abrindo projeto...", + searching: "Buscando...", empty: "Comece digitando um caminho", errors: { directory_not_found: "Diretório não encontrado.", diff --git a/packages/app/src/i18n/resources/ru.ts b/packages/app/src/i18n/resources/ru.ts index 79aa70306..5e66f20fd 100644 --- a/packages/app/src/i18n/resources/ru.ts +++ b/packages/app/src/i18n/resources/ru.ts @@ -1077,6 +1077,7 @@ export const ru: TranslationResources = { projectPicker: { placeholder: "Введите путь к каталогу...", opening: "Открытие проекта...", + searching: "Идет поиск...", empty: "Начните вводить путь", errors: { directory_not_found: "Каталог не найден.", diff --git a/packages/app/src/i18n/resources/zh-CN.ts b/packages/app/src/i18n/resources/zh-CN.ts index abb29848e..4504959b6 100644 --- a/packages/app/src/i18n/resources/zh-CN.ts +++ b/packages/app/src/i18n/resources/zh-CN.ts @@ -1039,6 +1039,7 @@ export const zhCN: TranslationResources = { projectPicker: { placeholder: "输入目录路径...", opening: "正在打开 project...", + searching: "正在搜索...", empty: "开始输入路径", errors: { directory_not_found: "找不到目录。",