| author | |
| committer | |
| log | 6eed953a060213630af0d7e3cd9a664bd85f7ced |
| tree | 90231578e8d75f9846f6bb59a4ed5e3fed2abff8 |
| parent | 6651aaab38c6814ed7bba3e805a1ec7a01870b55 |
| signature | Signed by PGP key B5690EEEBB952194 |
3 files changed, 61 insertions(+), 34 deletions(-)
apps/mobile/src/features/home/HomeScreen.tsx+5-33| ... | @@ -67,7 +67,7 @@ import { useThreadListV2ShelfPreferences } from "../threads/use-thread-list-v2-s | ... | @@ -67,7 +67,7 @@ import { useThreadListV2ShelfPreferences } from "../threads/use-thread-list-v2-s |
| 67 | import type { HomeListFilterMenuEnvironment } from "./home-list-filter-menu"; | 67 | import type { HomeListFilterMenuEnvironment } from "./home-list-filter-menu"; |
| 68 | import { | 68 | import { |
| 69 | buildHomeProjectScopes, | 69 | buildHomeProjectScopes, |
| 70 | sortHomeProjectScopes, | 70 | findHomeProjectScope, |
| 71 | type HomeProjectSortOrder, | 71 | type HomeProjectSortOrder, |
| 72 | } from "./homeThreadList"; | 72 | } from "./homeThreadList"; |
| 73 | import { createSwipeRowActivation } from "./swipe-row-activation"; | 73 | import { createSwipeRowActivation } from "./swipe-row-activation"; |
| ... | @@ -386,42 +386,14 @@ export function HomeScreen(props: HomeScreenProps) { | ... | @@ -386,42 +386,14 @@ export function HomeScreen(props: HomeScreenProps) { |
| 386 | }, [props.projects]); | 386 | }, [props.projects]); |
| 387 | 387 | ||
| 388 | const v2ProjectScopeKey = props.selectedProjectKey; | 388 | const v2ProjectScopeKey = props.selectedProjectKey; |
| 389 | const v2ScopeProjects = useMemo( | ||
| 390 | () => | ||
| 391 | sortHomeProjectScopes({ | ||
| 392 | scopes: projectScopes, | ||
| 393 | threads: props.threads, | ||
| 394 | pendingTasks: props.pendingTasks, | ||
| 395 | projectSortOrder: props.projectSortOrder, | ||
| 396 | }), | ||
| 397 | [ | ||
| 398 | props.pendingTasks, | ||
| 399 | props.projects, | ||
| 400 | props.projectSortOrder, | ||
| 401 | props.selectedEnvironmentId, | ||
| 402 | props.threads, | ||
| 403 | projectScopes, | ||
| 404 | ], | ||
| 405 | ); | ||
| 406 | const v2ScopedProjectGroup = useMemo( | 389 | const v2ScopedProjectGroup = useMemo( |
| 407 | () => | 390 | () => findHomeProjectScope(projectScopes, v2ProjectScopeKey), |
| 408 | v2ProjectScopeKey === null | 391 | [v2ProjectScopeKey, projectScopes], |
| 409 | ? null | ||
| 410 | : (v2ScopeProjects.find( | ||
| 411 | (scope) => | ||
| 412 | scope.key === v2ProjectScopeKey || | ||
| 413 | scope.projectRefs.some( | ||
| 414 | (projectRef) => | ||
| 415 | scopedProjectKey(projectRef.environmentId, projectRef.projectId) === | ||
| 416 | v2ProjectScopeKey, | ||
| 417 | ), | ||
| 418 | ) ?? null), | ||
| 419 | [v2ProjectScopeKey, v2ScopeProjects], | ||
| 420 | ); | 392 | ); |
| 421 | const v2ProjectTitleByProjectKey = useMemo( | 393 | const v2ProjectTitleByProjectKey = useMemo( |
| 422 | () => | 394 | () => |
| 423 | new Map( | 395 | new Map( |
| 424 | v2ScopeProjects.flatMap((scope) => | 396 | projectScopes.flatMap((scope) => |
| 425 | scope.projectRefs.map( | 397 | scope.projectRefs.map( |
| 426 | (projectRef) => | 398 | (projectRef) => |
| 427 | [ | 399 | [ |
| ... | @@ -431,7 +403,7 @@ export function HomeScreen(props: HomeScreenProps) { | ... | @@ -431,7 +403,7 @@ export function HomeScreen(props: HomeScreenProps) { |
| 431 | ), | 403 | ), |
| 432 | ), | 404 | ), |
| 433 | ), | 405 | ), |
| 434 | [v2ScopeProjects], | 406 | [projectScopes], |
| 435 | ); | 407 | ); |
| 436 | const v2ScopedProjectKeys = useMemo( | 408 | const v2ScopedProjectKeys = useMemo( |
| 437 | () => | 409 | () => |
apps/mobile/src/features/home/homeThreadList.test.ts+36-1| ... | @@ -5,7 +5,11 @@ import type { | ... | @@ -5,7 +5,11 @@ import type { |
| 5 | import { EnvironmentId, ProjectId, ProviderInstanceId, ThreadId } from "@t3tools/contracts"; | 5 | import { EnvironmentId, ProjectId, ProviderInstanceId, ThreadId } from "@t3tools/contracts"; |
| 6 | import { describe, expect, it } from "vite-plus/test"; | 6 | import { describe, expect, it } from "vite-plus/test"; |
| 7 | 7 | ||
| 8 | import { buildHomeProjectScopes, sortHomeProjectScopes } from "./homeThreadList"; | 8 | import { |
| 9 | buildHomeProjectScopes, | ||
| 10 | findHomeProjectScope, | ||
| 11 | sortHomeProjectScopes, | ||
| 12 | } from "./homeThreadList"; | ||
| 9 | import { makeThreadShellFixture } from "../../test-fixtures"; | 13 | import { makeThreadShellFixture } from "../../test-fixtures"; |
| 10 | 14 | ||
| 11 | function makeProject( | 15 | function makeProject( |
| ... | @@ -282,4 +286,35 @@ describe("home project scopes", () => { | ... | @@ -282,4 +286,35 @@ describe("home project scopes", () => { |
| 282 | }), | 286 | }), |
| 283 | ).toHaveLength(2); | 287 | ).toHaveLength(2); |
| 284 | }); | 288 | }); |
| 289 | |||
| 290 | it("prefers an exact scope key over a project ID that collides with it", () => { | ||
| 291 | const environmentId = EnvironmentId.make("environment-local"); | ||
| 292 | // project.create accepts caller-supplied IDs, so one project's scoped ref | ||
| 293 | // key can equal another project's normalized workspace-root scope key. | ||
| 294 | const collidingProject = makeProject({ | ||
| 295 | environmentId, | ||
| 296 | id: ProjectId.make("/workspaces/target"), | ||
| 297 | title: "Colliding", | ||
| 298 | workspaceRoot: "/workspaces/colliding", | ||
| 299 | }); | ||
| 300 | const targetProject = makeProject({ | ||
| 301 | environmentId, | ||
| 302 | id: ProjectId.make("project-target"), | ||
| 303 | title: "Target", | ||
| 304 | workspaceRoot: "/workspaces/target", | ||
| 305 | }); | ||
| 306 | const scopes = buildHomeProjectScopes({ | ||
| 307 | projects: [collidingProject, targetProject], | ||
| 308 | environmentId: null, | ||
| 309 | projectGroupingMode: "separate", | ||
| 310 | }); | ||
| 311 | const targetScope = scopes.find((scope) => scope.title === "Target"); | ||
| 312 | expect(scopes.map((scope) => scope.title)).toEqual(["Colliding", "Target"]); | ||
| 313 | expect(targetScope?.key).toBe("environment-local:/workspaces/target"); | ||
| 314 | |||
| 315 | expect(findHomeProjectScope(scopes, "environment-local:/workspaces/target")).toBe(targetScope); | ||
| 316 | expect(findHomeProjectScope(scopes, "environment-local:project-target")).toBe(targetScope); | ||
| 317 | expect(findHomeProjectScope(scopes, "environment-local:missing")).toBeNull(); | ||
| 318 | expect(findHomeProjectScope(scopes, null)).toBeNull(); | ||
| 319 | }); | ||
| 285 | }); | 320 | }); |
apps/mobile/src/features/home/homeThreadList.ts+20| ... | @@ -65,6 +65,26 @@ export function buildHomeProjectScopes(input: { | ... | @@ -65,6 +65,26 @@ export function buildHomeProjectScopes(input: { |
| 65 | }); | 65 | }); |
| 66 | } | 66 | } |
| 67 | 67 | ||
| 68 | // Prefers an exact scope key so a caller-supplied project ID that equals another | ||
| 69 | // project's normalized workspace root cannot select the wrong group. | ||
| 70 | export function findHomeProjectScope( | ||
| 71 | scopes: ReadonlyArray<HomeProjectScope>, | ||
| 72 | key: string | null, | ||
| 73 | ): HomeProjectScope | null { | ||
| 74 | if (key === null) { | ||
| 75 | return null; | ||
| 76 | } | ||
| 77 | return ( | ||
| 78 | scopes.find((scope) => scope.key === key) ?? | ||
| 79 | scopes.find((scope) => | ||
| 80 | scope.projectRefs.some( | ||
| 81 | (projectRef) => scopedProjectKey(projectRef.environmentId, projectRef.projectId) === key, | ||
| 82 | ), | ||
| 83 | ) ?? | ||
| 84 | null | ||
| 85 | ); | ||
| 86 | } | ||
| 87 | |||
| 68 | export function sortHomeProjectScopes(input: { | 88 | export function sortHomeProjectScopes(input: { |
| 69 | readonly scopes: ReadonlyArray<HomeProjectScope>; | 89 | readonly scopes: ReadonlyArray<HomeProjectScope>; |
| 70 | readonly threads: ReadonlyArray<EnvironmentThreadShell>; | 90 | readonly threads: ReadonlyArray<EnvironmentThreadShell>; |