diff --git a/apps/web/src/components/settings/ProjectSettingsPanel.logic.test.ts b/apps/web/src/components/settings/ProjectSettingsPanel.logic.test.ts index 8d0ad779842c..41e1688d2fe9 100644 --- a/apps/web/src/components/settings/ProjectSettingsPanel.logic.test.ts +++ b/apps/web/src/components/settings/ProjectSettingsPanel.logic.test.ts @@ -1,7 +1,97 @@ import { describe, expect, it } from "vite-plus/test"; -import { EnvironmentId, ProjectId, ThreadId } from "@t3tools/contracts"; +import { EnvironmentId, ProjectId, SourceControlProviderKind, ThreadId } from "@t3tools/contracts"; import { planProjectRemoval } from "@t3tools/client-runtime/state/projects"; +import { buildSidebarProjectSnapshots } from "../../sidebarProjectGrouping"; +import type { Project } from "../../types"; +import { scopeProjectSettingsGroup } from "./ProjectSettingsPanel.logic"; + +const localEnvironmentId = EnvironmentId.make("local"); +const remoteEnvironmentId = EnvironmentId.make("remote"); +const customIcon = { kind: "lucide", name: "cloud", color: "red" } as const; + +function makeProject(environmentId: EnvironmentId, overrides: Partial = {}): Project { + return { + environmentId, + id: ProjectId.make(`${environmentId}-project`), + title: "repo", + workspaceRoot: `/${environmentId}/repo`, + repositoryIdentity: { + canonicalKey: "github.com/owner/repo", + locator: { + source: "git-remote", + remoteName: "origin", + remoteUrl: "https://github.com/owner/repo.git", + }, + provider: SourceControlProviderKind.make("github"), + owner: "owner", + name: "repo", + displayName: "owner/repo", + }, + defaultModelSelection: null, + faviconPath: null, + projectIcon: null, + scripts: [], + createdAt: "2026-07-01T00:00:00.000Z", + updatedAt: "2026-07-01T00:00:00.000Z", + ...overrides, + }; +} + +function projectGroup(projects: Project[]) { + return buildSidebarProjectSnapshots({ + projects, + settings: { sidebarProjectGroupingMode: "repository", sidebarProjectGroupingOverrides: {} }, + primaryEnvironmentId: localEnvironmentId, + resolveEnvironmentLabel: (id) => id, + })[0]!; +} + +describe("scopeProjectSettingsGroup", () => { + it("keeps the sidebar preview identity and edit targets when the remote checkout was registered first", () => { + const local = makeProject(localEnvironmentId, { + title: "Local repo", + faviconPath: "local.svg", + projectIcon: customIcon, + }); + const remote = makeProject(remoteEnvironmentId, { + title: "Remote repo", + faviconPath: "remote.svg", + }); + const group = projectGroup([remote, local]); + const scoped = scopeProjectSettingsGroup(group, group.memberProjects); + + expect(group).toMatchObject(local); + expect(scoped).toMatchObject(local); + expect(scoped.memberProjects).toBe(group.memberProjects); + expect(scoped.memberProjects).toMatchObject([remote, local]); + }); + + it.each([ + [remoteEnvironmentId, "local-project"], + [localEnvironmentId, "other-checkout"], + ])("uses the complete selected checkout identity for %s/%s", (environmentId, id) => { + const local = makeProject(localEnvironmentId, { projectIcon: customIcon }); + const selected = makeProject(environmentId, { + id: ProjectId.make(id), + title: "Selected checkout", + workspaceRoot: "/selected/repo", + faviconPath: "selected.svg", + projectIcon: null, + }); + const group = projectGroup([local, selected]); + const members = group.memberProjects.filter( + (member) => member.environmentId === selected.environmentId && member.id === selected.id, + ); + const scoped = scopeProjectSettingsGroup(group, members); + + expect(scoped).toMatchObject(selected); + expect(scoped.memberProjects).toBe(members); + expect(scoped.memberProjects).toMatchObject([selected]); + expect(group).toMatchObject(local); + }); +}); + describe("project removal confirmation", () => { const local = { id: ProjectId.make("shared-id"), diff --git a/apps/web/src/components/settings/ProjectSettingsPanel.logic.ts b/apps/web/src/components/settings/ProjectSettingsPanel.logic.ts index 0c4650978c92..0c93b87ae162 100644 --- a/apps/web/src/components/settings/ProjectSettingsPanel.logic.ts +++ b/apps/web/src/components/settings/ProjectSettingsPanel.logic.ts @@ -1,5 +1,9 @@ import type { ProjectIconOverride } from "@t3tools/contracts"; +import type { + SidebarProjectGroupMember, + SidebarProjectSnapshot, +} from "../../sidebarProjectGrouping"; import type { Project } from "../../types"; export const SCRATCH_PROJECT_TITLE = "No project"; @@ -9,6 +13,17 @@ const SCRATCH_PROJECT_ICON = { color: "gray", } as const satisfies ProjectIconOverride; +export function scopeProjectSettingsGroup( + group: SidebarProjectSnapshot, + members: readonly SidebarProjectGroupMember[], +): SidebarProjectSnapshot { + const representative = + members.find( + (member) => member.environmentId === group.environmentId && member.id === group.id, + ) ?? members[0]!; + return { ...group, ...representative, memberProjects: members }; +} + export function projectGroupTitleNeedsUpdate( memberTitles: ReadonlyArray, nextTitle: string, diff --git a/apps/web/src/components/settings/ProjectSettingsPanel.tsx b/apps/web/src/components/settings/ProjectSettingsPanel.tsx index 379be839e2e2..fe0ac1e4e152 100644 --- a/apps/web/src/components/settings/ProjectSettingsPanel.tsx +++ b/apps/web/src/components/settings/ProjectSettingsPanel.tsx @@ -55,6 +55,7 @@ import { hasDefaultProjectIcon, projectGroupTitleNeedsUpdate, SCRATCH_PROJECT_TITLE, + scopeProjectSettingsGroup, } from "./ProjectSettingsPanel.logic"; import { useSettingsProjectGroups } from "./useSettingsProjectGroups"; import { readUnloadedThreadCount } from "../../state/olderSettledShelf"; @@ -235,12 +236,7 @@ export function ProjectSettingsPanel({ This checkout is no longer available in the selected project and environment.

); - const scopedGroup = { - ...selected, - memberProjects: members, - environmentId: members[0]!.environmentId, - id: members[0]!.id, - }; + const scopedGroup = scopeProjectSettingsGroup(selected, members); return ( editableIds.has(member.environmentId), ); - const representative = + const filePickerProject = group.memberProjects.find( (member) => environmentById.get(member.environmentId)?.serverConfig != null, ) ?? group.memberProjects[0]!; @@ -281,8 +277,8 @@ function ProjectDetail({ }); const projectNameEditedRef = useRef(false); - const faviconPath = representative.faviconPath ?? null; - const projectIcon = representative.projectIcon ?? null; + const faviconPath = group.faviconPath ?? null; + const projectIcon = group.projectIcon ?? null; const pickProjectFavicon = typeof window !== "undefined" && group.memberProjects.every( @@ -582,7 +578,7 @@ function ProjectDetail({ } control={
- +