mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
fix(sidebar): keep project header labels from switching to the repo name (#16877)
This commit is contained in:
+57
-1
@@ -52,6 +52,61 @@ describe('buildRows with pinned worktrees', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('keeps the cross-host project header label when another project section renders', () => {
|
||||
// Why: the project header must read the project's own display name, not the
|
||||
// anchor checkout's repo name, however many sections are visible — toggling
|
||||
// "Hide sleeping workspaces" must not rename a project (#16127).
|
||||
const otherRepo: Repo = {
|
||||
...repo,
|
||||
id: 'repo-other',
|
||||
path: '/tmp/design-assets',
|
||||
displayName: 'design-assets'
|
||||
}
|
||||
const otherWorktree: Worktree = {
|
||||
...worktree,
|
||||
id: 'wt-other',
|
||||
repoId: otherRepo.id,
|
||||
path: '/tmp/design-assets-feature',
|
||||
displayName: 'palette'
|
||||
}
|
||||
const buildHeaders = (extraWorktrees: Worktree[], extraRepos: Repo[]) => {
|
||||
const worktrees = [worktree, remoteWorktree, ...extraWorktrees]
|
||||
const rows = buildRows(
|
||||
'repo',
|
||||
worktrees,
|
||||
new Map([
|
||||
[repo.id, repo],
|
||||
[remoteRepo.id, remoteRepo],
|
||||
...extraRepos.map((entry): [string, Repo] => [entry.id, entry])
|
||||
]),
|
||||
null,
|
||||
new Set(),
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
{},
|
||||
new Map(worktrees.map((entry) => [entry.id, entry])),
|
||||
false,
|
||||
undefined,
|
||||
[],
|
||||
new Set(),
|
||||
new Map(),
|
||||
new Map(),
|
||||
[],
|
||||
{ projects: [project], projectHostSetups }
|
||||
)
|
||||
return rows.filter((row) => row.type === 'header')
|
||||
}
|
||||
|
||||
expect(buildHeaders([], [])).toMatchObject([
|
||||
{ key: 'project:github:stablyai/orca', label: 'Orca' }
|
||||
])
|
||||
expect(buildHeaders([otherWorktree], [otherRepo])).toMatchObject([
|
||||
{ key: 'project:github:stablyai/orca', label: 'Orca' },
|
||||
{ key: 'repo:repo-other', label: 'design-assets' }
|
||||
])
|
||||
})
|
||||
|
||||
it('renders same-project records with git remote identity as one mixed-host project header', () => {
|
||||
const localRepo: Repo = {
|
||||
...repo,
|
||||
@@ -724,11 +779,12 @@ describe('buildRows with pinned worktrees', () => {
|
||||
])
|
||||
// The provisioned copy nests under the plain project key with only its own
|
||||
// worktree; it never gets a path-scoped `::setup:` header like the real
|
||||
// checkouts do. (buildRows disambiguates its visible label to the repo name.)
|
||||
// checkouts do, and that header keeps the project's own display name.
|
||||
expect(
|
||||
headers.some((row) => row.key === 'project:github:stablyai/orca::setup:repo-runtime-b')
|
||||
).toBe(false)
|
||||
expect(headers.find((row) => row.key === 'project:github:stablyai/orca')).toMatchObject({
|
||||
label: 'Orca',
|
||||
count: 1
|
||||
})
|
||||
})
|
||||
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* Sidebar section headers disambiguate identical display names by appending
|
||||
* parent path segments. Every tracked repo is projected into a project, so
|
||||
* `project:` headers — not just `repo:` ones — are where collisions surface;
|
||||
* stabilising which model layer supplies the label (#16127) must not turn the
|
||||
* disambiguation off.
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { buildRows } from './worktree-list/grouping/build-rows'
|
||||
import { project, projectHostSetups, repo, worktree } from './worktree-list-groups-test-fixtures'
|
||||
import type { Project, ProjectHostSetup } from '../../../../shared/project-types'
|
||||
import type { Repo } from '../../../../shared/repo-types'
|
||||
import type { Worktree } from '../../../../shared/worktree/types'
|
||||
|
||||
const makeProject = (id: string, repoId: string): Project => ({
|
||||
...project,
|
||||
id,
|
||||
displayName: 'api',
|
||||
sourceRepoIds: [repoId]
|
||||
})
|
||||
|
||||
const makeSetup = (checkout: Repo, projectId: string): ProjectHostSetup => ({
|
||||
...projectHostSetups[0]!,
|
||||
id: checkout.id,
|
||||
projectId,
|
||||
repoId: checkout.id,
|
||||
path: checkout.path,
|
||||
displayName: checkout.displayName
|
||||
})
|
||||
|
||||
function buildHeaders(repos: Repo[], projects: Project[], setups: ProjectHostSetup[]) {
|
||||
const worktrees = repos.map(
|
||||
(entry): Worktree => ({ ...worktree, id: `wt-${entry.id}`, repoId: entry.id })
|
||||
)
|
||||
const rows = buildRows(
|
||||
'repo',
|
||||
worktrees,
|
||||
new Map(repos.map((entry) => [entry.id, entry])),
|
||||
null,
|
||||
new Set(),
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
{},
|
||||
new Map(worktrees.map((entry) => [entry.id, entry])),
|
||||
false,
|
||||
undefined,
|
||||
[],
|
||||
new Set(),
|
||||
new Map(),
|
||||
new Map(),
|
||||
[],
|
||||
{ projects, projectHostSetups: setups }
|
||||
)
|
||||
return rows.filter((row) => row.type === 'header')
|
||||
}
|
||||
|
||||
describe('sidebar section headers with colliding display names', () => {
|
||||
const workRepo: Repo = { ...repo, id: 'repo-work', path: '/work/api', displayName: 'api' }
|
||||
const ossRepo: Repo = { ...repo, id: 'repo-oss', path: '/oss/api', displayName: 'api' }
|
||||
|
||||
it('path-disambiguates two project headers that share a display name', () => {
|
||||
expect(
|
||||
buildHeaders(
|
||||
[workRepo, ossRepo],
|
||||
[makeProject('github:acme/api', workRepo.id), makeProject('gitlab:me/api', ossRepo.id)],
|
||||
[makeSetup(workRepo, 'github:acme/api'), makeSetup(ossRepo, 'gitlab:me/api')]
|
||||
)
|
||||
).toMatchObject([
|
||||
{ key: 'project:github:acme/api', label: 'work/api' },
|
||||
{ key: 'project:gitlab:me/api', label: 'oss/api' }
|
||||
])
|
||||
})
|
||||
|
||||
it('leaves a lone project header un-suffixed', () => {
|
||||
expect(
|
||||
buildHeaders(
|
||||
[workRepo],
|
||||
[makeProject('github:acme/api', workRepo.id)],
|
||||
[makeSetup(workRepo, 'github:acme/api')]
|
||||
)
|
||||
).toMatchObject([{ key: 'project:github:acme/api', label: 'api' }])
|
||||
})
|
||||
|
||||
it('path-disambiguates untracked repo headers that share a display name', () => {
|
||||
expect(buildHeaders([workRepo, ossRepo], [], [])).toMatchObject([
|
||||
{ key: 'repo:repo-work', label: 'work/api' },
|
||||
{ key: 'repo:repo-oss', label: 'oss/api' }
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -73,16 +73,21 @@ export function orderMainWorktreeFirst(worktrees: Worktree[]): Worktree[] {
|
||||
return [...mainWorktrees, ...worktrees.filter((worktree) => !worktree.isMainWorktree)]
|
||||
}
|
||||
|
||||
// Why: disambiguate a section's *own* label, not its anchor checkout's repo
|
||||
// name. Substituting repo.displayName made a project header read the project
|
||||
// name while it was the only repo-backed section and the checkout name as soon
|
||||
// as a second one rendered, so filtering workspaces renamed the project
|
||||
// (#16127). Path suffixes still resolve genuinely identical labels.
|
||||
export function withRepoSectionDisplayLabels(
|
||||
entries: readonly OrderedGroupEntry[]
|
||||
): OrderedGroupEntry[] {
|
||||
const repos = entries
|
||||
.map((entry) => entry[1].repo)
|
||||
.filter((repo): repo is Repo => repo !== undefined)
|
||||
if (repos.length < 2) {
|
||||
const labelItems = entries.flatMap(([, group]) =>
|
||||
group.repo ? [{ ...group.repo, displayName: group.label }] : []
|
||||
)
|
||||
if (labelItems.length === 0) {
|
||||
return [...entries]
|
||||
}
|
||||
const labelsByPath = getRepoDisplayLabelsByPath(repos)
|
||||
const labelsByPath = getRepoDisplayLabelsByPath(labelItems)
|
||||
return entries.map(([key, group]) => [
|
||||
key,
|
||||
group.repo
|
||||
|
||||
Reference in New Issue
Block a user