diff --git a/apps/mobile/src/features/settings/SettingsProjectOverviewRouteScreen.tsx b/apps/mobile/src/features/settings/SettingsProjectOverviewRouteScreen.tsx index 89408243ef81a..05ef2af346daa 100644 --- a/apps/mobile/src/features/settings/SettingsProjectOverviewRouteScreen.tsx +++ b/apps/mobile/src/features/settings/SettingsProjectOverviewRouteScreen.tsx @@ -138,7 +138,11 @@ function ProjectOverviewContent(props: { }) { const { isRemoving, setIsRemoving, startRemoval, finishRemoval } = props; const representative = props.members[0]!; - const displayName = deriveProjectGroupLabel({ representative, members: props.members }); + const displayName = deriveProjectGroupLabel({ + groupKey: props.group.key, + representative, + members: props.members, + }); const [draftName, setDraftName] = useState(null); const [isSaving, setIsSaving] = useState(false); const threads = useThreadShells(); diff --git a/apps/mobile/src/features/settings/SettingsRouteScreen.tsx b/apps/mobile/src/features/settings/SettingsRouteScreen.tsx index 50ad81383addc..bde67d793aeb4 100644 --- a/apps/mobile/src/features/settings/SettingsRouteScreen.tsx +++ b/apps/mobile/src/features/settings/SettingsRouteScreen.tsx @@ -252,8 +252,9 @@ function useSettingsIndexSections(): SettingsListSection[] { selectedTargets.some((target) => target.environmentId === project.environmentId), ) ?? []; const projectLabel = - scopedProjectMembers.length > 0 + selectedProject && scopedProjectMembers.length > 0 ? deriveProjectGroupLabel({ + groupKey: selectedProject.key, representative: scopedProjectMembers[0]!, members: scopedProjectMembers, }) diff --git a/packages/client-runtime/src/state/projectGrouping.test.ts b/packages/client-runtime/src/state/projectGrouping.test.ts index f4edba2b8d456..a890c852bcbf3 100644 --- a/packages/client-runtime/src/state/projectGrouping.test.ts +++ b/packages/client-runtime/src/state/projectGrouping.test.ts @@ -303,6 +303,50 @@ describe("buildProjectGroups", () => { expect(label(renamed)).toBe("t3code"); }); + it("labels a group by its repository when clones disagree on its casing", () => { + // A subfolder project, its repository root, and a clone whose remote URL is + // lowercased: no shared title or display name, but one repository. + const subfolder = makeProject("design-study", "/work/NewCo/reference/design-study"); + const root = makeProject("NewCo", "/work/NewCo"); + const lowercasedClone = makeProject("NewCo", "/remote/NewCo", { + environmentId: EnvironmentId.make("remote"), + repositoryIdentity: { ...repositoryIdentity, name: "T3CODE", displayName: "t3 code" }, + }); + + const groups = buildProjectGroups({ + projects: [subfolder, root, lowercasedClone], + settings: settings("repository"), + preferredEnvironmentId: environmentId, + }); + expect(groups).toHaveLength(1); + expect(groups[0]?.label).toBe("T3 Code"); + + // A subfolder group in repository_path mode keeps its own title. + const localIdentity = { ...repositoryIdentity, rootPath: "/work/t3code" }; + const pathGroups = buildProjectGroups({ + projects: [ + makeProject("design", "/work/t3code/design", { + title: "Design docs", + hasCustomTitle: true, + repositoryIdentity: localIdentity, + }), + makeProject("design", "/remote/t3code/design", { + environmentId: EnvironmentId.make("remote"), + repositoryIdentity: { + ...repositoryIdentity, + rootPath: "/remote/t3code", + name: "T3CODE", + displayName: "t3 code", + }, + }), + makeProject("t3code", "/work/t3code", { repositoryIdentity: localIdentity }), + ], + settings: settings("repository_path"), + preferredEnvironmentId: environmentId, + }); + expect(pathGroups.map((group) => group.label)).toEqual(["Design docs", "t3code"]); + }); + it("applies a physical-project override without dropping its siblings", () => { const first = makeProject("t3code", "/work/t3code"); const second = makeProject("t3code-2", "/work/t3code-2"); diff --git a/packages/client-runtime/src/state/projectGrouping.ts b/packages/client-runtime/src/state/projectGrouping.ts index c05a9724ce207..b54342b5dfcb6 100644 --- a/packages/client-runtime/src/state/projectGrouping.ts +++ b/packages/client-runtime/src/state/projectGrouping.ts @@ -199,6 +199,7 @@ export function resolveScratchProjectScopeKey( } export function deriveProjectGroupLabel(input: { + readonly groupKey: string; readonly representative: Pick; readonly members: ReadonlyArray< Pick @@ -230,6 +231,19 @@ export function deriveProjectGroupLabel(input: { return sharedRepositoryNames[0]!; } + // A whole-repository group is named for the repository, not one member's folder: + // clones can disagree on its casing. A subfolder group keeps its own title. + const representativeIdentity = input.representative.repositoryIdentity; + if ( + representativeIdentity && + input.groupKey === repositoryGroupingKeyOf(representativeIdentity) + ) { + return ( + repositoryGroupingDisplayNameOf(representativeIdentity) ?? + representativeIdentity.name ?? + input.representative.title + ); + } return input.representative.title; } @@ -369,6 +383,7 @@ export function buildProjectGroups(input: { ? "No project" : members.length > 1 ? deriveProjectGroupLabel({ + groupKey: key, representative, members: members.map((member) => member.project), })