Fix manual project ordering for identity headers (#5933)

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Jinwoo Hong
2026-06-21 13:23:21 -07:00
committed by GitHub
co-authored by Orca
parent c01c0462ae
commit 67fe4bd780
2 changed files with 115 additions and 3 deletions
@@ -361,6 +361,82 @@ describe('buildRows with pinned worktrees', () => {
])
})
it('orders project identity headers by the manual repo order anchor', () => {
const analyticsProject: Project = {
...project,
id: 'github:stablyai/analytics',
displayName: 'Analytics',
sourceRepoIds: ['repo-analytics']
}
const analyticsRepo: Repo = {
...repo,
id: 'repo-analytics',
path: '/tmp/analytics',
displayName: 'analytics',
upstream: { owner: 'stablyai', repo: 'analytics' }
}
const analyticsWorktree: Worktree = {
...worktree,
id: 'wt-analytics',
repoId: analyticsRepo.id,
displayName: 'analytics'
}
const analyticsSetup: ProjectHostSetup = {
...projectHostSetups[0]!,
id: analyticsRepo.id,
projectId: analyticsProject.id,
repoId: analyticsRepo.id,
path: analyticsRepo.path,
displayName: analyticsRepo.displayName
}
const repoOrder = new Map([
[repo.id, 0],
[remoteRepo.id, 1],
[analyticsRepo.id, 2]
])
const rows = buildRows(
'repo',
[worktree, analyticsWorktree, remoteWorktree],
new Map([
[repo.id, repo],
[remoteRepo.id, remoteRepo],
[analyticsRepo.id, analyticsRepo]
]),
null,
new Set(),
repoOrder,
undefined,
'manual',
{},
new Map([
[worktree.id, worktree],
[remoteWorktree.id, remoteWorktree],
[analyticsWorktree.id, analyticsWorktree]
]),
false,
undefined,
[],
new Set(),
new Map(),
[],
{
projects: [project, analyticsProject],
projectHostSetups: [...projectHostSetups, analyticsSetup]
}
)
const headers = rows.filter((row) => row.type === 'header')
expect(headers.map((row) => row.key)).toEqual([
'project:github:stablyai/orca',
'project:github:stablyai/analytics'
])
expect(headers[0]).toMatchObject({
key: 'project:github:stablyai/orca',
repo: { id: repo.id, badgeColor: repo.badgeColor }
})
})
it('splits same-host checkouts of one project into separate per-setup groups', () => {
// Why: multiple local clones/worktrees of one repo share the GitHub slug, so
// collapsing to the project would merge them into one arbitrarily-named group.
@@ -673,9 +673,39 @@ function manualRankForEntry(
repoOrder: Map<string, number> | undefined
): number {
const key = entry[0]
const repoId = key.startsWith('repo:') ? key.slice('repo:'.length) : key
const rank = repoOrder?.get(repoId)
return rank === undefined ? Number.POSITIVE_INFINITY : rank
const repoIds =
entry[1].repoIds.size > 0
? [...entry[1].repoIds]
: [key.startsWith('repo:') ? key.slice('repo:'.length) : key]
let rank = Number.POSITIVE_INFINITY
for (const repoId of repoIds) {
const repoRank = repoOrder?.get(repoId)
if (repoRank !== undefined && repoRank < rank) {
rank = repoRank
}
}
return rank
}
function getManualOrderAnchorRepo(
group: WorktreeGroupEntry,
repoMap: Map<string, Repo>,
repoOrder: Map<string, number> | undefined
): Repo | undefined {
let anchor = group.repo
let anchorRank = anchor ? (repoOrder?.get(anchor.id) ?? Number.POSITIVE_INFINITY) : undefined
for (const repoId of group.repoIds) {
const repo = repoMap.get(repoId)
if (!repo) {
continue
}
const rank = repoOrder?.get(repoId) ?? Number.POSITIVE_INFINITY
if (!anchor || rank < (anchorRank ?? Number.POSITIVE_INFINITY)) {
anchor = repo
anchorRank = rank
}
}
return anchor
}
/**
@@ -904,6 +934,12 @@ export function buildRows(
}
}
} else {
for (const group of grouped.values()) {
// Why: logical project headers can contain multiple host setup repos.
// Use the repo that anchors manual order so drag and actions target the
// same persisted order source the row sorter reads.
group.repo = getManualOrderAnchorRepo(group, repoMap, repoOrder)
}
// Why: project header order is its own user choice (projectOrderBy),
// decoupled from workspace sortBy. Manual uses the canonical repoOrder so
// header drag has a stable source of truth; Recent follows activity.