diff --git a/apps/mobile/src/features/threads/NewTaskRouteScreen.tsx b/apps/mobile/src/features/threads/NewTaskRouteScreen.tsx index 3a6f6ed7a9..96d2cb87b8 100644 --- a/apps/mobile/src/features/threads/NewTaskRouteScreen.tsx +++ b/apps/mobile/src/features/threads/NewTaskRouteScreen.tsx @@ -7,8 +7,16 @@ import { } from "@react-navigation/native"; import { SymbolView } from "../../components/AppSymbol"; import type { EnvironmentProject } from "@t3tools/client-runtime/state/shell"; -import { useEffect, useRef } from "react"; -import { ActivityIndicator, Alert, Platform, Pressable, ScrollView, View } from "react-native"; +import { useEffect, useRef, useState } from "react"; +import { + ActivityIndicator, + Alert, + Platform, + Pressable, + ScrollView, + TextInput, + View, +} from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { cn } from "../../lib/cn"; @@ -22,6 +30,7 @@ import { useAdaptiveWorkspaceLayout } from "../layout/AdaptiveWorkspaceLayout"; import { useIncomingShare } from "../sharing/IncomingShareProvider"; import { useNewTaskFlow } from "./new-task-flow-provider"; import { + filterProjectScopes, getProjectScopeAccessibilityLabel, getProjectScopeSelectionTarget, } from "./new-task-project-selection"; @@ -87,6 +96,7 @@ function deriveProjectEmptyState(catalogState: WorkspaceState): { export function NewTaskRouteScreen({ route }: StaticScreenProps) { const projects = useProjects(); + const [searchText, setSearchText] = useState(""); const { projectScopes, selectedEnvironmentId, setProject } = useNewTaskFlow(); const { state: catalogState } = useWorkspaceState(); const navigation = useNavigation(); @@ -107,6 +117,7 @@ export function NewTaskRouteScreen({ route }: StaticScreenProps(null); const reservedDestinationProject = incomingShare?.destination ? (projects.find( @@ -230,9 +241,46 @@ export function NewTaskRouteScreen({ route }: StaticScreenProps )} + + + + + {searchText.length > 0 ? ( + setSearchText("")} + > + + + ) : null} + + + )} + ) : visibleScopes.length === 0 ? ( + + + No matching projects + + + Try a different project name or workspace path. + + ) : ( - {projectScopes.map((scope, scopeIndex) => { + {visibleScopes.map((scope, scopeIndex) => { const hasMultipleProjects = scope.projects.length > 1; const selectionTarget = getProjectScopeSelectionTarget(scope, selectedEnvironmentId); return ( diff --git a/apps/mobile/src/features/threads/new-task-project-selection.test.ts b/apps/mobile/src/features/threads/new-task-project-selection.test.ts index 4ed3cbfde6..c78c056d16 100644 --- a/apps/mobile/src/features/threads/new-task-project-selection.test.ts +++ b/apps/mobile/src/features/threads/new-task-project-selection.test.ts @@ -4,12 +4,43 @@ import { describe, expect, it } from "vite-plus/test"; import type { EnvironmentProject } from "@t3tools/client-runtime/state/shell"; import type { HomeProjectScope } from "../home/homeThreadList"; import { + filterProjectScopes, getProjectScopeAccessibilityLabel, getProjectScopeSelectionTarget, resolveDraftProjectSelection, resolveEnvironmentProjectMatch, } from "./new-task-project-selection"; +describe("filterProjectScopes", () => { + const local = makeProject("code", "mac", { title: "Desktop checkout" }); + const remote = makeProject("remote-code", "server", { + workspaceRoot: "/srv/remote-workspace", + }); + const code = makeScope([local, remote]); + const docs = { ...makeScope([makeProject("docs")]), key: "docs", title: "Documentation" }; + const scopes = [code, docs]; + + it("keeps all scopes for an empty or whitespace query", () => { + expect(filterProjectScopes(scopes, "")).toBe(scopes); + expect(filterProjectScopes(scopes, " ")).toBe(scopes); + }); + + it("finds logical names, project titles and workspace paths without case sensitivity", () => { + expect(filterProjectScopes(scopes, " T3 CODE ")).toEqual([code]); + expect(filterProjectScopes(scopes, "DESKTOP")).toEqual([code]); + expect(filterProjectScopes(scopes, "REMOTE-WORKSPACE")).toEqual([code]); + expect(filterProjectScopes(scopes, "documentation")).toEqual([docs]); + expect(filterProjectScopes(scopes, "missing-project")).toEqual([]); + }); + + it("keeps every workspace and the preferred environment when one workspace matches", () => { + const matches = filterProjectScopes(scopes, "REMOTE-WORKSPACE"); + expect(matches[0]).toBe(code); + expect(getProjectScopeSelectionTarget(matches[0]!, EnvironmentId.make("mac"))).toBe(local); + expect(code.projects).toEqual([local, remote]); + }); +}); + function makeProject( id: string, environmentId = "environment", diff --git a/apps/mobile/src/features/threads/new-task-project-selection.ts b/apps/mobile/src/features/threads/new-task-project-selection.ts index 05fc9e2cfe..4b7aecce04 100644 --- a/apps/mobile/src/features/threads/new-task-project-selection.ts +++ b/apps/mobile/src/features/threads/new-task-project-selection.ts @@ -28,6 +28,23 @@ export function getProjectScopeAccessibilityLabel( : `${scope.title}, ${selectionTarget.workspaceRoot}`; } +export function filterProjectScopes( + scopes: ReadonlyArray, + searchText: string, +): ReadonlyArray { + const query = searchText.trim().toLowerCase(); + if (!query) return scopes; + return scopes.filter( + (scope) => + scope.title.toLowerCase().includes(query) || + scope.projects.some( + (project) => + project.title.toLowerCase().includes(query) || + project.workspaceRoot.toLowerCase().includes(query), + ), + ); +} + function getOnlySelectableProject( projectScopes: ReadonlyArray, ): EnvironmentProject | null {