mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 08:37:49 +00:00
fix(projects): a project move names the row it moves; a paired server's twin by its SSH target (#30794)
* fix(projects): a project move names the row it moves; a paired server's twin by its SSH target * fix(sidebar): pass the header's row without an assertion; e2e helpers and focus baseline follow the row-shaped move * fix(sidebar): a header drop re-reads the dragged row and drops nothing if its group changed mid-drag * fix(sidebar): read live repos through a stable store reader, not a ref written during render
This commit is contained in:
@@ -391,7 +391,7 @@
|
||||
3 src/renderer/src/store/github/work-item-fetch-actions.ts
|
||||
2 src/renderer/src/store/github/work-item-routing.ts
|
||||
9 src/renderer/src/store/index.ts
|
||||
3 src/renderer/src/store/project-groups/project-group-mutations.ts
|
||||
2 src/renderer/src/store/project-groups/project-group-mutations.ts
|
||||
1 src/renderer/src/store/repos/all-host-repo-catalog-actions.ts
|
||||
1 src/renderer/src/store/repos/converted-ssh-browser-pages.ts
|
||||
2 src/renderer/src/store/repos/repo-add-actions.ts
|
||||
|
||||
@@ -296,7 +296,7 @@ export abstract class OrcaRuntimeWithPreservedBranchCleanup extends OrcaRuntimeW
|
||||
|
||||
protected readonly projectGroups = new RuntimeProjectGroupController({
|
||||
getStore: () => this.store,
|
||||
resolveRepo: (selector) => this.resolveRepoSelector(selector),
|
||||
resolveRepo: (...args) => this.managedWorktreeQueries.resolveRepoForConnection(...args),
|
||||
notifyReposChanged: () => this.notifyReposChanged(),
|
||||
resolveFolderConnectionId: (workspace) => this.resolveFolderWorkspaceConnectionId(workspace),
|
||||
teardownFolderWorkspacePtys: (worktreeId, connectionId) =>
|
||||
|
||||
@@ -714,7 +714,8 @@ describe('repo RPC methods', () => {
|
||||
makeRequest('projectGroup.moveProject', {
|
||||
repo: 'repo-1',
|
||||
groupId: group.id,
|
||||
order: 2
|
||||
order: 2,
|
||||
connectionId: 'srv-ssh'
|
||||
})
|
||||
)
|
||||
const folderListResponse = await dispatcher.dispatch(makeRequest('folderWorkspace.list'))
|
||||
@@ -757,7 +758,7 @@ describe('repo RPC methods', () => {
|
||||
isCollapsed: true
|
||||
})
|
||||
expect(runtime.deleteProjectGroup).toHaveBeenCalledWith(group.id)
|
||||
expect(runtime.moveProjectToGroup).toHaveBeenCalledWith('repo-1', group.id, 2)
|
||||
expect(runtime.moveProjectToGroup).toHaveBeenCalledWith('repo-1', group.id, 2, 'srv-ssh')
|
||||
expect(runtime.listVisibleFolderWorkspaces).toHaveBeenCalled()
|
||||
expect(runtime.createFolderWorkspace).toHaveBeenCalledWith({
|
||||
projectGroupId: group.id,
|
||||
|
||||
@@ -78,7 +78,12 @@ export const REPO_METHODS = [
|
||||
params: ProjectGroupMoveProject,
|
||||
handler: async (params, context) => ({
|
||||
repo: projectRepoVisibilityForClient(
|
||||
await context.runtime.moveProjectToGroup(params.repo, params.groupId ?? null, params.order),
|
||||
await context.runtime.moveProjectToGroup(
|
||||
params.repo,
|
||||
params.groupId ?? null,
|
||||
params.order,
|
||||
params.connectionId
|
||||
),
|
||||
context
|
||||
)
|
||||
})
|
||||
|
||||
@@ -28,7 +28,8 @@ import {
|
||||
|
||||
type RuntimeProjectGroupDependencies = {
|
||||
getStore: () => RuntimeStore | null
|
||||
resolveRepo: (selector: string) => Promise<Repo>
|
||||
// `connectionId` narrows twins by their SSH target; undefined keeps the plain selector.
|
||||
resolveRepo: (selector: string, connectionId?: string | null) => Promise<Repo>
|
||||
notifyReposChanged: () => void
|
||||
resolveFolderConnectionId: (workspace: FolderWorkspace) => string | null
|
||||
teardownFolderWorkspacePtys: (worktreeId: string, connectionId: string | null) => Promise<void>
|
||||
@@ -128,12 +129,17 @@ export class RuntimeProjectGroupController {
|
||||
return { deleted }
|
||||
}
|
||||
|
||||
async moveProject(repoSelector: string, groupId: string | null, order?: number): Promise<Repo> {
|
||||
async moveProject(
|
||||
repoSelector: string,
|
||||
groupId: string | null,
|
||||
order?: number,
|
||||
connectionId?: string | null
|
||||
): Promise<Repo> {
|
||||
const store = this.deps.getStore()
|
||||
if (!store?.moveProjectToGroup) {
|
||||
throw new Error('runtime_unavailable')
|
||||
}
|
||||
const repo = await this.deps.resolveRepo(repoSelector)
|
||||
const repo = await this.deps.resolveRepo(repoSelector, connectionId)
|
||||
assertWslMoveRepoWritable(store, repo)
|
||||
const moved = store.moveProjectToGroup(repo, groupId, order)
|
||||
if (!moved) {
|
||||
|
||||
@@ -123,6 +123,22 @@ export function registerRuntimeRemoveProjectHostScopeTests(): void {
|
||||
)
|
||||
expect(repos.map((repo) => repo.projectGroupId)).toEqual([undefined, undefined])
|
||||
})
|
||||
|
||||
it('moves the SSH row a paired client names by its connection', async () => {
|
||||
const { runtime, repos } = createRuntime()
|
||||
|
||||
await runtime.moveProjectToGroup('dup', 'group-1', undefined, 'ssh-1')
|
||||
|
||||
expect(repos.map((repo) => repo.projectGroupId)).toEqual([undefined, 'group-1'])
|
||||
})
|
||||
|
||||
it('moves the local row a paired client names with no connection', async () => {
|
||||
const { runtime, repos } = createRuntime()
|
||||
|
||||
await runtime.moveProjectToGroup('dup', 'group-1', undefined, null)
|
||||
|
||||
expect(repos.map((repo) => repo.projectGroupId)).toEqual(['group-1', undefined])
|
||||
})
|
||||
})
|
||||
|
||||
describe('repo.update with the same repo id on two execution hosts', () => {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { commitProjectHeaderDragDrop } from './project-header-drag-commit'
|
||||
import type { ProjectHeaderDragSession } from './project-header-drag-contract'
|
||||
import { getProjectHeaderDragBucketKey } from './project-header-drop'
|
||||
import type { Repo } from '../../../../shared/repo-types'
|
||||
|
||||
function makeRepo(id: string, overrides: Partial<Repo> = {}): Repo {
|
||||
@@ -17,12 +18,12 @@ function makeRepo(id: string, overrides: Partial<Repo> = {}): Repo {
|
||||
}
|
||||
|
||||
function makeSession(
|
||||
repoId: string,
|
||||
repo: Repo,
|
||||
sidebarRepoHeaderIds: readonly string[]
|
||||
): ProjectHeaderDragSession {
|
||||
return {
|
||||
repoId,
|
||||
bucketKey: 'ungrouped',
|
||||
repo,
|
||||
bucketKey: getProjectHeaderDragBucketKey(repo),
|
||||
sidebarRepoHeaderIds,
|
||||
pointerId: 1,
|
||||
headerRects: [],
|
||||
@@ -41,10 +42,11 @@ describe('commitProjectHeaderDragDrop', () => {
|
||||
const repoById = new Map(repos.map((repo) => [repo.id, repo]))
|
||||
|
||||
commitProjectHeaderDragDrop({
|
||||
session: makeSession('c', ['a', 'b', 'c']),
|
||||
session: makeSession(makeRepo('c'), ['a', 'b', 'c']),
|
||||
sidebarDropIndex: 0,
|
||||
orderedRepoIds: ['a', 'b', 'c'],
|
||||
repoById,
|
||||
liveRepos: repos,
|
||||
usesProjectGroupOrdering: false,
|
||||
onCommitRepoOrder,
|
||||
onCommitProjectGroupOrder: vi.fn()
|
||||
@@ -59,10 +61,11 @@ describe('commitProjectHeaderDragDrop', () => {
|
||||
const repoById = new Map(repos.map((repo) => [repo.id, repo]))
|
||||
|
||||
commitProjectHeaderDragDrop({
|
||||
session: makeSession('same', ['b', 'same', 'c']),
|
||||
session: makeSession(makeRepo('same'), ['b', 'same', 'c']),
|
||||
sidebarDropIndex: 0,
|
||||
orderedRepoIds: ['b', 'same', 'c', 'same'],
|
||||
repoById,
|
||||
liveRepos: repos,
|
||||
usesProjectGroupOrdering: false,
|
||||
onCommitRepoOrder,
|
||||
onCommitProjectGroupOrder: vi.fn()
|
||||
@@ -77,10 +80,11 @@ describe('commitProjectHeaderDragDrop', () => {
|
||||
const repoById = new Map(repos.map((repo) => [repo.id, repo]))
|
||||
|
||||
commitProjectHeaderDragDrop({
|
||||
session: makeSession('same', ['b', 'same', 'c']),
|
||||
session: makeSession(makeRepo('same'), ['b', 'same', 'c']),
|
||||
sidebarDropIndex: 2,
|
||||
orderedRepoIds: ['b', 'same', 'c', 'same'],
|
||||
repoById,
|
||||
liveRepos: repos,
|
||||
usesProjectGroupOrdering: false,
|
||||
onCommitRepoOrder,
|
||||
onCommitProjectGroupOrder: vi.fn()
|
||||
@@ -89,25 +93,49 @@ describe('commitProjectHeaderDragDrop', () => {
|
||||
expect(onCommitRepoOrder).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('commits projectGroupOrder when project groups are present', () => {
|
||||
it('commits projectGroupOrder for the dragged row, not the id-keyed twin on another host', () => {
|
||||
const onCommitProjectGroupOrder = vi.fn()
|
||||
const repos = [
|
||||
makeRepo('a', { projectGroupId: 'group-1' }),
|
||||
makeRepo('b', { projectGroupId: 'group-1' }),
|
||||
makeRepo('c', { projectGroupId: 'group-1', connectionId: 'ssh-1' })
|
||||
makeRepo('c', { projectGroupId: 'group-1', executionHostId: 'local' })
|
||||
]
|
||||
const repoById = new Map(repos.map((repo) => [repo.id, repo]))
|
||||
const sshTwin = makeRepo('c', { projectGroupId: 'group-1', connectionId: 'ssh-1' })
|
||||
|
||||
commitProjectHeaderDragDrop({
|
||||
session: makeSession('c', ['a', 'b', 'c']),
|
||||
session: makeSession(sshTwin, ['a', 'b', 'c']),
|
||||
sidebarDropIndex: 0,
|
||||
orderedRepoIds: ['a', 'b', 'c'],
|
||||
repoById,
|
||||
liveRepos: [...repos, sshTwin],
|
||||
usesProjectGroupOrdering: true,
|
||||
onCommitRepoOrder: vi.fn(),
|
||||
onCommitProjectGroupOrder
|
||||
})
|
||||
|
||||
expect(onCommitProjectGroupOrder).toHaveBeenCalledWith('c', 'group-1', -1, 'ssh:ssh-1')
|
||||
expect(onCommitProjectGroupOrder).toHaveBeenCalledWith(sshTwin, 'group-1', -1)
|
||||
})
|
||||
|
||||
it('drops nothing when the dragged row changed group during the drag', () => {
|
||||
const onCommitProjectGroupOrder = vi.fn()
|
||||
const repos = [
|
||||
makeRepo('a', { projectGroupId: 'group-1' }),
|
||||
makeRepo('b', { projectGroupId: 'group-1' })
|
||||
]
|
||||
const dragged = makeRepo('c', { projectGroupId: 'group-1' })
|
||||
|
||||
commitProjectHeaderDragDrop({
|
||||
session: makeSession(dragged, ['a', 'b', 'c']),
|
||||
sidebarDropIndex: 0,
|
||||
orderedRepoIds: ['a', 'b', 'c'],
|
||||
repoById: new Map([...repos, dragged].map((repo) => [repo.id, repo])),
|
||||
liveRepos: [...repos, { ...dragged, projectGroupId: 'group-2' }],
|
||||
usesProjectGroupOrdering: true,
|
||||
onCommitRepoOrder: vi.fn(),
|
||||
onCommitProjectGroupOrder
|
||||
})
|
||||
|
||||
expect(onCommitProjectGroupOrder).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,35 +1,33 @@
|
||||
import { getLogicalRepoOrderRankById } from '../../../../shared/project-section-order'
|
||||
import {
|
||||
applyAllRepoInsertAt,
|
||||
getProjectHeaderDragBucketKey,
|
||||
getProjectGroupOrderForSidebarDrop,
|
||||
mapSidebarProjectHeaderDropIndexToSiblingInsertIndex,
|
||||
mapSidebarRepoDropIndexToAllRepoInsertAt
|
||||
} from './project-header-drop'
|
||||
import type { ProjectHeaderDragSession } from './project-header-drag-contract'
|
||||
import type { Repo } from '../../../../shared/repo-types'
|
||||
import { getRepoExecutionHostId, type ExecutionHostId } from '../../../../shared/execution-host'
|
||||
import { isSameRepoRow } from '@/store/slices/repo-host-identity'
|
||||
|
||||
export function commitProjectHeaderDragDrop(args: {
|
||||
session: ProjectHeaderDragSession
|
||||
sidebarDropIndex: number
|
||||
orderedRepoIds: readonly string[]
|
||||
repoById: ReadonlyMap<string, Repo>
|
||||
liveRepos: readonly Repo[]
|
||||
usesProjectGroupOrdering: boolean
|
||||
onCommitRepoOrder: (orderedIds: string[]) => void
|
||||
onCommitProjectGroupOrder: (
|
||||
repoId: string,
|
||||
projectGroupId: string | null,
|
||||
order: number,
|
||||
hostId: ExecutionHostId
|
||||
) => void
|
||||
onCommitProjectGroupOrder: (project: Repo, projectGroupId: string | null, order: number) => void
|
||||
}): void {
|
||||
const draggedRepo = args.repoById.get(args.session.repoId)
|
||||
if (!draggedRepo) {
|
||||
const draggedRepo = args.liveRepos.find((repo) => isSameRepoRow(repo, args.session.repo))
|
||||
// Why: a group change mid-drag (another window or client) moved the row out of this bucket.
|
||||
if (!draggedRepo || getProjectHeaderDragBucketKey(draggedRepo) !== args.session.bucketKey) {
|
||||
return
|
||||
}
|
||||
|
||||
const sidebarRepoHeaderIds = args.session.sidebarRepoHeaderIds
|
||||
const sourceIndex = sidebarRepoHeaderIds.indexOf(args.session.repoId)
|
||||
const sourceIndex = sidebarRepoHeaderIds.indexOf(draggedRepo.id)
|
||||
// Why: both slots bordering the dragged header are visual no-ops. In
|
||||
// particular, do not compact paired-host occurrences on an unchanged drop.
|
||||
if (
|
||||
@@ -42,7 +40,7 @@ export function commitProjectHeaderDragDrop(args: {
|
||||
|
||||
if (args.usesProjectGroupOrdering) {
|
||||
const siblings = sidebarRepoHeaderIds
|
||||
.filter((repoId) => repoId !== args.session.repoId)
|
||||
.filter((repoId) => repoId !== draggedRepo.id)
|
||||
.map((repoId) => args.repoById.get(repoId))
|
||||
.filter((repo): repo is Repo => repo !== undefined)
|
||||
const siblingDropIndex = mapSidebarProjectHeaderDropIndexToSiblingInsertIndex({
|
||||
@@ -65,13 +63,7 @@ export function commitProjectHeaderDragDrop(args: {
|
||||
dropIndex: siblingDropIndex,
|
||||
repoOrderRankById
|
||||
})
|
||||
// Why the dragged row's host: its group and order came from that row, so main writes that twin.
|
||||
args.onCommitProjectGroupOrder(
|
||||
args.session.repoId,
|
||||
draggedRepo.projectGroupId ?? null,
|
||||
order,
|
||||
getRepoExecutionHostId(draggedRepo)
|
||||
)
|
||||
args.onCommitProjectGroupOrder(draggedRepo, draggedRepo.projectGroupId ?? null, order)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -80,7 +72,7 @@ export function commitProjectHeaderDragDrop(args: {
|
||||
sidebarRepoHeaderIds,
|
||||
args.orderedRepoIds
|
||||
)
|
||||
const next = applyAllRepoInsertAt(args.orderedRepoIds, args.session.repoId, insertAt)
|
||||
const next = applyAllRepoInsertAt(args.orderedRepoIds, draggedRepo.id, insertAt)
|
||||
if (!next) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import type { PointerEvent } from 'react'
|
||||
|
||||
import type { ProjectHeaderDragBucketKey, ProjectHeaderDragRect } from './project-header-drop'
|
||||
import type { Repo } from '../../../../shared/repo-types'
|
||||
import type { ExecutionHostId } from '../../../../shared/execution-host'
|
||||
|
||||
export type RepoDragState = {
|
||||
draggingRepoId: string | null
|
||||
@@ -20,24 +19,21 @@ export type UseRepoHeaderDragArgs = {
|
||||
orderedRepoIds: string[]
|
||||
sidebarRepoHeaderIdsByBucket: ReadonlyMap<ProjectHeaderDragBucketKey, readonly string[]>
|
||||
repoById: ReadonlyMap<string, Repo>
|
||||
getLiveRepos: () => readonly Repo[]
|
||||
usesProjectGroupOrdering: boolean
|
||||
onCommitRepoOrder: (orderedIds: string[]) => void
|
||||
onCommitProjectGroupOrder: (
|
||||
repoId: string,
|
||||
projectGroupId: string | null,
|
||||
order: number,
|
||||
hostId: ExecutionHostId
|
||||
) => void
|
||||
onCommitProjectGroupOrder: (project: Repo, projectGroupId: string | null, order: number) => void
|
||||
getScrollContainer: () => HTMLElement | null
|
||||
}
|
||||
|
||||
export type RepoHeaderDragController = {
|
||||
state: RepoDragState
|
||||
onHandlePointerDown: (event: PointerEvent<HTMLElement>, repoId: string) => void
|
||||
onHandlePointerDown: (event: PointerEvent<HTMLElement>, repo: Repo) => void
|
||||
}
|
||||
|
||||
export type ProjectHeaderDragSession = {
|
||||
repoId: string
|
||||
// Why the row, not its id: a twin on another host shares the id.
|
||||
repo: Repo
|
||||
bucketKey: ProjectHeaderDragBucketKey
|
||||
sidebarRepoHeaderIds: readonly string[]
|
||||
pointerId: number
|
||||
|
||||
@@ -24,7 +24,6 @@ describe('createProjectHeaderDragSession', () => {
|
||||
const scrollContainer = document.createElement('div')
|
||||
document.body.append(scrollContainer, handleEl)
|
||||
|
||||
const repoById = new Map<string, Repo>([['repo-a', createRepo('repo-a')]])
|
||||
const sidebarRepoHeaderIdsByBucket = new Map([['ungrouped', ['repo-a', 'repo-b']]])
|
||||
|
||||
const session = createProjectHeaderDragSession({
|
||||
@@ -36,8 +35,7 @@ describe('createProjectHeaderDragSession', () => {
|
||||
target: handleEl,
|
||||
currentTarget: handleEl
|
||||
} as unknown as React.PointerEvent<HTMLElement>,
|
||||
repoId: 'repo-a',
|
||||
repoById,
|
||||
repo: createRepo('repo-a'),
|
||||
sidebarRepoHeaderIdsByBucket,
|
||||
getScrollContainer: () => scrollContainer
|
||||
})
|
||||
@@ -54,7 +52,6 @@ describe('createProjectHeaderDragSession', () => {
|
||||
const scrollContainer = document.createElement('div')
|
||||
document.body.append(scrollContainer, header)
|
||||
|
||||
const repoById = new Map<string, Repo>([['repo-a', createRepo('repo-a')]])
|
||||
const sidebarRepoHeaderIdsByBucket = new Map([['ungrouped', ['repo-a', 'repo-b']]])
|
||||
|
||||
const session = createProjectHeaderDragSession({
|
||||
@@ -66,13 +63,12 @@ describe('createProjectHeaderDragSession', () => {
|
||||
target: label,
|
||||
currentTarget: header
|
||||
} as unknown as React.PointerEvent<HTMLElement>,
|
||||
repoId: 'repo-a',
|
||||
repoById,
|
||||
repo: createRepo('repo-a'),
|
||||
sidebarRepoHeaderIdsByBucket,
|
||||
getScrollContainer: () => scrollContainer
|
||||
})
|
||||
|
||||
expect(session?.repoId).toBe('repo-a')
|
||||
expect(session?.repo.id).toBe('repo-a')
|
||||
})
|
||||
|
||||
it('arms a drag session when pressing the project icon svg (SVGElement target)', () => {
|
||||
@@ -85,7 +81,6 @@ describe('createProjectHeaderDragSession', () => {
|
||||
const scrollContainer = document.createElement('div')
|
||||
document.body.append(scrollContainer, header)
|
||||
|
||||
const repoById = new Map<string, Repo>([['repo-a', createRepo('repo-a')]])
|
||||
const sidebarRepoHeaderIdsByBucket = new Map([['ungrouped', ['repo-a', 'repo-b']]])
|
||||
|
||||
const session = createProjectHeaderDragSession({
|
||||
@@ -97,13 +92,12 @@ describe('createProjectHeaderDragSession', () => {
|
||||
target: iconSvg,
|
||||
currentTarget: header
|
||||
} as unknown as React.PointerEvent<HTMLElement>,
|
||||
repoId: 'repo-a',
|
||||
repoById,
|
||||
repo: createRepo('repo-a'),
|
||||
sidebarRepoHeaderIdsByBucket,
|
||||
getScrollContainer: () => scrollContainer
|
||||
})
|
||||
|
||||
expect(session?.repoId).toBe('repo-a')
|
||||
expect(session?.repo.id).toBe('repo-a')
|
||||
})
|
||||
|
||||
it('does not arm a drag session when pressing an svg icon inside an action button', () => {
|
||||
@@ -119,7 +113,6 @@ describe('createProjectHeaderDragSession', () => {
|
||||
const scrollContainer = document.createElement('div')
|
||||
document.body.append(scrollContainer, header)
|
||||
|
||||
const repoById = new Map<string, Repo>([['repo-a', createRepo('repo-a')]])
|
||||
const sidebarRepoHeaderIdsByBucket = new Map([['ungrouped', ['repo-a', 'repo-b']]])
|
||||
|
||||
const session = createProjectHeaderDragSession({
|
||||
@@ -131,8 +124,7 @@ describe('createProjectHeaderDragSession', () => {
|
||||
target: actionIcon,
|
||||
currentTarget: header
|
||||
} as unknown as React.PointerEvent<HTMLElement>,
|
||||
repoId: 'repo-a',
|
||||
repoById,
|
||||
repo: createRepo('repo-a'),
|
||||
sidebarRepoHeaderIdsByBucket,
|
||||
getScrollContainer: () => scrollContainer
|
||||
})
|
||||
@@ -154,7 +146,6 @@ describe('createProjectHeaderDragSession', () => {
|
||||
const scrollContainer = document.createElement('div')
|
||||
document.body.append(scrollContainer, header)
|
||||
|
||||
const repoById = new Map<string, Repo>([['repo-a', createRepo('repo-a')]])
|
||||
const sidebarRepoHeaderIdsByBucket = new Map([['ungrouped', ['repo-a', 'repo-b']]])
|
||||
|
||||
const sessionFromActions = createProjectHeaderDragSession({
|
||||
@@ -166,8 +157,7 @@ describe('createProjectHeaderDragSession', () => {
|
||||
target: actions,
|
||||
currentTarget: header
|
||||
} as unknown as React.PointerEvent<HTMLElement>,
|
||||
repoId: 'repo-a',
|
||||
repoById,
|
||||
repo: createRepo('repo-a'),
|
||||
sidebarRepoHeaderIdsByBucket,
|
||||
getScrollContainer: () => scrollContainer
|
||||
})
|
||||
@@ -180,8 +170,7 @@ describe('createProjectHeaderDragSession', () => {
|
||||
target: label,
|
||||
currentTarget: header
|
||||
} as unknown as React.PointerEvent<HTMLElement>,
|
||||
repoId: 'repo-a',
|
||||
repoById,
|
||||
repo: createRepo('repo-a'),
|
||||
sidebarRepoHeaderIdsByBucket,
|
||||
getScrollContainer: () => scrollContainer
|
||||
})
|
||||
@@ -194,15 +183,14 @@ describe('createProjectHeaderDragSession', () => {
|
||||
target: header,
|
||||
currentTarget: header
|
||||
} as unknown as React.PointerEvent<HTMLElement>,
|
||||
repoId: 'repo-a',
|
||||
repoById,
|
||||
repo: createRepo('repo-a'),
|
||||
sidebarRepoHeaderIdsByBucket,
|
||||
getScrollContainer: () => scrollContainer
|
||||
})
|
||||
|
||||
expect(sessionFromActions).toBeNull()
|
||||
expect(sessionFromLabel?.repoId).toBe('repo-a')
|
||||
expect(sessionFromLabel?.repo.id).toBe('repo-a')
|
||||
// Why: target === currentTarget is treated as the empty row (not an action).
|
||||
expect(sessionFromRowPadding?.repoId).toBe('repo-a')
|
||||
expect(sessionFromRowPadding?.repo.id).toBe('repo-a')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -14,8 +14,7 @@ import type { Repo } from '../../../../shared/repo-types'
|
||||
|
||||
export function createProjectHeaderDragSession(args: {
|
||||
event: PointerEvent<HTMLElement>
|
||||
repoId: string
|
||||
repoById: ReadonlyMap<string, Repo>
|
||||
repo: Repo
|
||||
sidebarRepoHeaderIdsByBucket: ReadonlyMap<ProjectHeaderDragBucketKey, readonly string[]>
|
||||
getScrollContainer: () => HTMLElement | null
|
||||
}): ProjectHeaderDragSession | null {
|
||||
@@ -28,11 +27,7 @@ export function createProjectHeaderDragSession(args: {
|
||||
if (isRepoHeaderActionTarget(args.event.target, args.event.currentTarget)) {
|
||||
return null
|
||||
}
|
||||
const repo = args.repoById.get(args.repoId)
|
||||
if (!repo) {
|
||||
return null
|
||||
}
|
||||
const bucketKey = getProjectHeaderDragBucketKey(repo)
|
||||
const bucketKey = getProjectHeaderDragBucketKey(args.repo)
|
||||
const sidebarRepoHeaderIds = args.sidebarRepoHeaderIdsByBucket.get(bucketKey) ?? []
|
||||
// Why: a single project in its bucket has nowhere to land, so skip arming
|
||||
// drag and let the header click toggle collapse instead.
|
||||
@@ -47,7 +42,7 @@ export function createProjectHeaderDragSession(args: {
|
||||
// Why: defer setPointerCapture until the drag threshold is crossed so a
|
||||
// header click still reaches the inner collapse handler on pointerup.
|
||||
return {
|
||||
repoId: args.repoId,
|
||||
repo: args.repo,
|
||||
bucketKey,
|
||||
sidebarRepoHeaderIds,
|
||||
pointerId: args.event.pointerId,
|
||||
|
||||
@@ -17,6 +17,7 @@ import { createProjectHeaderDragSession } from './project-header-drag-start'
|
||||
import { getWorktreeSidebarDragAutoscroll } from './worktree-sidebar-drag-autoscroll'
|
||||
import { hasPointerBeenReleased } from './header-drag-pointer-release'
|
||||
import { swallowNextClickOnDragHandle } from './header-drag-click-swallow'
|
||||
import type { Repo } from '../../../../shared/repo-types'
|
||||
|
||||
// Why pointer events instead of HTML5 DnD: rows are absolutely-positioned by
|
||||
// react-virtual and unmount/remount as scroll changes, so DnD enter/leave fire
|
||||
@@ -27,6 +28,7 @@ export function useRepoHeaderDrag({
|
||||
orderedRepoIds,
|
||||
sidebarRepoHeaderIdsByBucket,
|
||||
repoById,
|
||||
getLiveRepos,
|
||||
usesProjectGroupOrdering,
|
||||
onCommitRepoOrder,
|
||||
onCommitProjectGroupOrder,
|
||||
@@ -46,6 +48,8 @@ export function useRepoHeaderDrag({
|
||||
usesProjectGroupOrderingRef.current = usesProjectGroupOrdering
|
||||
const onCommitRepoOrderRef = useRef(onCommitRepoOrder)
|
||||
onCommitRepoOrderRef.current = onCommitRepoOrder
|
||||
// Read once: callers pass a stable store reader.
|
||||
const getLiveReposRef = useRef(getLiveRepos)
|
||||
const onCommitProjectGroupOrderRef = useRef(onCommitProjectGroupOrder)
|
||||
onCommitProjectGroupOrderRef.current = onCommitProjectGroupOrder
|
||||
const getContainerRef = useRef(getScrollContainer)
|
||||
@@ -144,6 +148,7 @@ export function useRepoHeaderDrag({
|
||||
sidebarDropIndex,
|
||||
orderedRepoIds: orderedIdsRef.current,
|
||||
repoById: repoByIdRef.current,
|
||||
liveRepos: getLiveReposRef.current(),
|
||||
usesProjectGroupOrdering: usesProjectGroupOrderingRef.current,
|
||||
onCommitRepoOrder: onCommitRepoOrderRef.current,
|
||||
onCommitProjectGroupOrder: onCommitProjectGroupOrderRef.current
|
||||
@@ -177,7 +182,7 @@ export function useRepoHeaderDrag({
|
||||
refreshHeaderRects()
|
||||
}
|
||||
|
||||
applyDrop(session.repoId, computeDrop(session.latestPointerY))
|
||||
applyDrop(session.repo.id, computeDrop(session.latestPointerY))
|
||||
|
||||
autoscrollFrameIdRef.current = window.requestAnimationFrame(runAutoscrollFrame)
|
||||
},
|
||||
@@ -227,10 +232,10 @@ export function useRepoHeaderDrag({
|
||||
}
|
||||
}
|
||||
refreshHeaderRects()
|
||||
setState({ draggingRepoId: session.repoId, dropIndex: null, dropIndicatorY: null })
|
||||
setState({ draggingRepoId: session.repo.id, dropIndex: null, dropIndicatorY: null })
|
||||
}
|
||||
refreshHeaderRects()
|
||||
applyDrop(session.repoId, computeDrop(e.clientY))
|
||||
applyDrop(session.repo.id, computeDrop(e.clientY))
|
||||
ensureAutoscroll()
|
||||
}
|
||||
const onPointerUp = (e: PointerEvent): void => {
|
||||
@@ -296,23 +301,19 @@ export function useRepoHeaderDrag({
|
||||
}
|
||||
}, [state.draggingRepoId])
|
||||
|
||||
const onHandlePointerDown = useCallback(
|
||||
(event: React.PointerEvent<HTMLElement>, repoId: string) => {
|
||||
const session = createProjectHeaderDragSession({
|
||||
event,
|
||||
repoId,
|
||||
repoById: repoByIdRef.current,
|
||||
sidebarRepoHeaderIdsByBucket: sidebarRepoHeaderIdsByBucketRef.current,
|
||||
getScrollContainer: getContainerRef.current
|
||||
})
|
||||
if (!session) {
|
||||
return
|
||||
}
|
||||
dragSessionRef.current = session
|
||||
setSessionArmed(true)
|
||||
},
|
||||
[]
|
||||
)
|
||||
const onHandlePointerDown = useCallback((event: React.PointerEvent<HTMLElement>, repo: Repo) => {
|
||||
const session = createProjectHeaderDragSession({
|
||||
event,
|
||||
repo,
|
||||
sidebarRepoHeaderIdsByBucket: sidebarRepoHeaderIdsByBucketRef.current,
|
||||
getScrollContainer: getContainerRef.current
|
||||
})
|
||||
if (!session) {
|
||||
return
|
||||
}
|
||||
dragSessionRef.current = session
|
||||
setSessionArmed(true)
|
||||
}, [])
|
||||
|
||||
return { state, onHandlePointerDown }
|
||||
}
|
||||
|
||||
@@ -78,10 +78,9 @@ export function useWorktreeContextMenuCommands(args: {
|
||||
if (!args.repo) {
|
||||
return
|
||||
}
|
||||
const hostId = getRepoExecutionHostId(args.repo)
|
||||
const group = await args.createProjectGroup(name, hostId)
|
||||
const group = await args.createProjectGroup(name, getRepoExecutionHostId(args.repo))
|
||||
if (group) {
|
||||
await args.moveProjectToGroup(args.repo.id, group.id, undefined, hostId)
|
||||
await args.moveProjectToGroup(args.repo, group.id)
|
||||
}
|
||||
},
|
||||
[args]
|
||||
@@ -91,18 +90,13 @@ export function useWorktreeContextMenuCommands(args: {
|
||||
if (!args.repo || args.repo.projectGroupId === groupId) {
|
||||
return
|
||||
}
|
||||
void args.moveProjectToGroup(
|
||||
args.repo.id,
|
||||
groupId,
|
||||
undefined,
|
||||
getRepoExecutionHostId(args.repo)
|
||||
)
|
||||
void args.moveProjectToGroup(args.repo, groupId)
|
||||
},
|
||||
[args]
|
||||
)
|
||||
const handleRemoveProjectFromGroup = useCallback(() => {
|
||||
if (args.repo) {
|
||||
void args.moveProjectToGroup(args.repo.id, null, undefined, getRepoExecutionHostId(args.repo))
|
||||
void args.moveProjectToGroup(args.repo, null)
|
||||
}
|
||||
}, [args])
|
||||
const handleAssignWorkspaceStatus = useCallback(
|
||||
|
||||
@@ -23,6 +23,8 @@ import { useProjectGroupHeaderDrag } from '../../project-group-header-drag'
|
||||
import { getSidebarOrderedProjectGroupHeaderIdsByBucket } from '../../project-group-header-drop'
|
||||
import { USER_SCROLL_MEASUREMENT_ADJUSTMENT_SUPPRESS_MS } from '../viewport/use-scroll-suppression'
|
||||
|
||||
const readLiveRepos = (): readonly Repo[] => useAppStore.getState().repos
|
||||
|
||||
export type WorktreeSidebarHeaderDrag = ReturnType<typeof useWorktreeSidebarHeaderDrag>
|
||||
|
||||
function indexById(byBucket: ReadonlyMap<string, readonly string[]>): Map<string, number> {
|
||||
@@ -160,8 +162,8 @@ export function useWorktreeSidebarHeaderDrag(args: {
|
||||
[sidebarProjectGroupHeaderIdsByBucket]
|
||||
)
|
||||
const commitProjectGroupOrder = useCallback(
|
||||
(repoId: string, projectGroupId: string | null, order: number, hostId: ExecutionHostId) => {
|
||||
void moveProjectToGroup(repoId, projectGroupId, order, hostId)
|
||||
(project: Repo, projectGroupId: string | null, order: number) => {
|
||||
void moveProjectToGroup(project, projectGroupId, order)
|
||||
},
|
||||
[moveProjectToGroup]
|
||||
)
|
||||
@@ -182,6 +184,7 @@ export function useWorktreeSidebarHeaderDrag(args: {
|
||||
orderedRepoIds: allRepoIds,
|
||||
sidebarRepoHeaderIdsByBucket,
|
||||
repoById: repoMap,
|
||||
getLiveRepos: readLiveRepos,
|
||||
usesProjectGroupOrdering: hasProjectGroups,
|
||||
onCommitRepoOrder: commitRepoReorder,
|
||||
onCommitProjectGroupOrder: commitProjectGroupOrder,
|
||||
|
||||
@@ -94,7 +94,8 @@ export function renderWorktreeSectionHeaderRow(args: {
|
||||
const { headerDrag } = ctx
|
||||
const isRepoHeader = ctx.groupBy === 'repo' && row.repo !== undefined
|
||||
const isProjectGroupHeader = ctx.groupBy === 'repo' && row.projectGroup !== undefined
|
||||
const projectIdForHeader = isRepoHeader ? row.repo!.id : undefined
|
||||
const headerRepo = isRepoHeader ? row.repo : undefined
|
||||
const projectIdForHeader = headerRepo?.id
|
||||
const projectGroupIdForHeader =
|
||||
isProjectGroupHeader && !row.repo && typeof row.projectGroup?.id === 'string'
|
||||
? row.projectGroup.id
|
||||
@@ -278,8 +279,8 @@ export function renderWorktreeSectionHeaderRow(args: {
|
||||
: undefined
|
||||
}
|
||||
onPointerDown={
|
||||
isDraggableRepoHeader && projectIdForHeader
|
||||
? (event) => headerDrag.repoDrag.onHandlePointerDown(event, projectIdForHeader)
|
||||
isDraggableRepoHeader && headerRepo
|
||||
? (event) => headerDrag.repoDrag.onHandlePointerDown(event, headerRepo)
|
||||
: isDraggableProjectGroupHeader && projectGroupIdForHeader
|
||||
? (event) =>
|
||||
headerDrag.projectGroupDrag.onHandlePointerDown(event, projectGroupIdForHeader)
|
||||
|
||||
@@ -89,14 +89,14 @@ export function useProjectGroupDialogs(args: {
|
||||
if (repo.projectGroupId === groupId) {
|
||||
return
|
||||
}
|
||||
void moveProjectToGroup(repo.id, groupId, undefined, getRepoExecutionHostId(repo))
|
||||
void moveProjectToGroup(repo, groupId)
|
||||
},
|
||||
[moveProjectToGroup]
|
||||
)
|
||||
|
||||
const handleRemoveProjectFromGroup = useCallback(
|
||||
(repo: Repo) => {
|
||||
void moveProjectToGroup(repo.id, null, undefined, getRepoExecutionHostId(repo))
|
||||
void moveProjectToGroup(repo, null)
|
||||
},
|
||||
[moveProjectToGroup]
|
||||
)
|
||||
@@ -114,10 +114,9 @@ export function useProjectGroupDialogs(args: {
|
||||
return
|
||||
}
|
||||
if (nameDialog.type === 'create-from-repo') {
|
||||
const hostId = getRepoExecutionHostId(nameDialog.repo)
|
||||
const group = await createProjectGroup(name, hostId)
|
||||
const group = await createProjectGroup(name, getRepoExecutionHostId(nameDialog.repo))
|
||||
if (group) {
|
||||
await moveProjectToGroup(nameDialog.repo.id, group.id, undefined, hostId)
|
||||
await moveProjectToGroup(nameDialog.repo, group.id)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -10,7 +10,11 @@ import {
|
||||
resolveProjectGroupOwnerHostId,
|
||||
runtimeTargetForProjectGroupOwner
|
||||
} from '../slices/project-group-owner-routing'
|
||||
import { findRepoForHost, repoMatchesHostIdentity } from '../slices/repo-host-identity'
|
||||
import {
|
||||
findRepoForHost,
|
||||
isSameRepoRow,
|
||||
repoMatchesHostIdentity
|
||||
} from '../slices/repo-host-identity'
|
||||
import { callRuntimeRpc } from '../../runtime/runtime-rpc-client'
|
||||
import { ipcOrRuntimeTargetForHostId } from '../../runtime/runtime-client-target'
|
||||
import { getRepoExecutionHostId } from '../../../../shared/execution-host'
|
||||
@@ -249,12 +253,12 @@ export function createProjectGroupMutationActions(
|
||||
}
|
||||
},
|
||||
|
||||
moveProjectToGroup: async (projectId, groupId, order, hostId) => {
|
||||
moveProjectToGroup: async (project, groupId, order) => {
|
||||
try {
|
||||
const repo = findRepoForHost(get().repos, projectId, { settings: get().settings, hostId })
|
||||
const repo = get().repos.find((candidate) => isSameRepoRow(candidate, project))
|
||||
const repoHostId = repo ? getRepoExecutionHostId(repo) : null
|
||||
const target = repoHostId ? ipcOrRuntimeTargetForHostId(repoHostId) : null
|
||||
if (!repoHostId || !target) {
|
||||
if (!repo || !repoHostId || !target) {
|
||||
return false
|
||||
}
|
||||
if (groupId && !isProjectGroupJoinableFromCatalog(get().projectGroups, groupId, target)) {
|
||||
@@ -264,7 +268,7 @@ export function createProjectGroupMutationActions(
|
||||
const moved =
|
||||
target.kind === 'local'
|
||||
? await window.api.projectGroups.moveProject({
|
||||
projectId,
|
||||
projectId: repo.id,
|
||||
groupId,
|
||||
order,
|
||||
hostId: repoHostId
|
||||
@@ -273,7 +277,8 @@ export function createProjectGroupMutationActions(
|
||||
await callRuntimeRpc<{ repo: Repo | null }>(
|
||||
target,
|
||||
'projectGroup.moveProject',
|
||||
{ repo: projectId, groupId, order },
|
||||
// Why connectionId: the server's own twins differ only by their SSH target there.
|
||||
{ repo: repo.id, groupId, order, connectionId: repo.connectionId ?? null },
|
||||
{ timeoutMs: 15_000 }
|
||||
)
|
||||
).repo
|
||||
@@ -283,8 +288,8 @@ export function createProjectGroupMutationActions(
|
||||
const ownedMoved = adoptFromEndpoint(target, { kind: 'repo', row: moved })
|
||||
const movedHostId = getRepoExecutionHostId(ownedMoved)
|
||||
set((s) => {
|
||||
const nextRepos = s.repos.map((repo) =>
|
||||
repoMatchesHostIdentity(repo, projectId, movedHostId) ? ownedMoved : repo
|
||||
const nextRepos = s.repos.map((candidate) =>
|
||||
isSameRepoRow(candidate, repo) ? ownedMoved : candidate
|
||||
)
|
||||
return {
|
||||
repos: nextRepos,
|
||||
|
||||
@@ -22,6 +22,7 @@ import type {
|
||||
ProjectUpdateArgs
|
||||
} from '../../../../shared/project-types'
|
||||
import type { Repo } from '../../../../shared/repo-types'
|
||||
import type { RepoHostTarget } from '../../../../shared/repo-host-identity'
|
||||
import type {
|
||||
FolderWorkspacePathStatus,
|
||||
FolderWorkspacePathStatusRequest
|
||||
@@ -257,11 +258,11 @@ export type RepoSlice = {
|
||||
groupId: string,
|
||||
options: DeleteProjectGroupWithContainedProjectsOptions
|
||||
) => Promise<DeleteProjectGroupWithContainedProjectsResult>
|
||||
// The row itself, not its id: twins share an id, and a paired server's twins share a host too.
|
||||
moveProjectToGroup: (
|
||||
projectId: string,
|
||||
project: RepoHostTarget,
|
||||
groupId: string | null,
|
||||
order?: number,
|
||||
hostId?: ExecutionHostId
|
||||
order?: number
|
||||
) => Promise<boolean>
|
||||
// options.hostId is required: a bare id can resolve to another host's row (#13071). Removes nothing when that host has no row.
|
||||
// options.errorFeedback defaults to 'silent' so bulk/background callers keep their own aggregate reporting.
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { Repo } from '../../../../shared/repo-types'
|
||||
import {
|
||||
getRepoExecutionHostId,
|
||||
getSettingsFocusedExecutionHostId,
|
||||
normalizeHostPart,
|
||||
type ExecutionHostId
|
||||
} from '../../../../shared/execution-host'
|
||||
import {
|
||||
@@ -18,6 +19,14 @@ export function getRepoHostIdentity(repo: RepoIdentityParts): HostScopedRepoKey
|
||||
return getRepoHostIdentityForParts(repo.id, getRepoExecutionHostId(repo))
|
||||
}
|
||||
|
||||
/** One exact row: a paired server's twins share its `runtime:` host and differ by nested SSH target. */
|
||||
export function isSameRepoRow(left: RepoIdentityParts, right: RepoIdentityParts): boolean {
|
||||
return (
|
||||
getRepoHostIdentity(left) === getRepoHostIdentity(right) &&
|
||||
normalizeHostPart(left.connectionId) === normalizeHostPart(right.connectionId)
|
||||
)
|
||||
}
|
||||
|
||||
export function repoMatchesHostIdentity(
|
||||
repo: RepoIdentityParts,
|
||||
repoId: string,
|
||||
|
||||
@@ -88,9 +88,9 @@ describe('moving a project into a group on another host', () => {
|
||||
it('refuses with a toast instead of sending a move that would ungroup the project', async () => {
|
||||
const store = storeWith([group('local-group', 'local'), group('remote-group', REMOTE_HOST)])
|
||||
|
||||
await expect(
|
||||
store.getState().moveProjectToGroup(remoteRepo.id, 'local-group', 0, REMOTE_HOST)
|
||||
).resolves.toBe(false)
|
||||
await expect(store.getState().moveProjectToGroup(remoteRepo, 'local-group', 0)).resolves.toBe(
|
||||
false
|
||||
)
|
||||
|
||||
expect(moveCalls()).toEqual([])
|
||||
expect(moveProject).not.toHaveBeenCalled()
|
||||
@@ -103,7 +103,7 @@ describe('moving a project into a group on another host', () => {
|
||||
const store = storeWith([group('shared', 'local'), group('shared', REMOTE_HOST)])
|
||||
|
||||
await expect(
|
||||
store.getState().moveProjectToGroup(remoteRepo.id, 'shared', undefined, REMOTE_HOST)
|
||||
store.getState().moveProjectToGroup(remoteRepo, 'shared', undefined)
|
||||
).resolves.toBe(true)
|
||||
|
||||
expect(moveCalls()).toHaveLength(1)
|
||||
@@ -116,7 +116,7 @@ describe('moving a project into a group on another host', () => {
|
||||
const store = storeWith([])
|
||||
|
||||
await expect(
|
||||
store.getState().moveProjectToGroup(remoteRepo.id, 'unknown-group', undefined, REMOTE_HOST)
|
||||
store.getState().moveProjectToGroup(remoteRepo, 'unknown-group', undefined)
|
||||
).resolves.toBe(false)
|
||||
|
||||
expect(moveCalls()).toHaveLength(1)
|
||||
@@ -128,9 +128,9 @@ describe('moving a project into a group on another host', () => {
|
||||
runtimeMoveReturns({ ...remoteRepo, projectGroupId: null })
|
||||
const store = storeWith([group('remote-group', REMOTE_HOST)])
|
||||
|
||||
await expect(
|
||||
store.getState().moveProjectToGroup(remoteRepo.id, null, undefined, REMOTE_HOST)
|
||||
).resolves.toBe(true)
|
||||
await expect(store.getState().moveProjectToGroup(remoteRepo, null, undefined)).resolves.toBe(
|
||||
true
|
||||
)
|
||||
|
||||
expect(toast.error).not.toHaveBeenCalled()
|
||||
})
|
||||
@@ -147,10 +147,35 @@ describe('moving one twin of a repo id shared by a local and an SSH row', () =>
|
||||
projectGroups: [group('ssh-group', 'ssh:ssh-1')]
|
||||
})
|
||||
|
||||
await store.getState().moveProjectToGroup(remoteRepo.id, 'ssh-group', undefined, 'ssh:ssh-1')
|
||||
await store.getState().moveProjectToGroup(sshTwin, 'ssh-group')
|
||||
|
||||
expect(moveProject).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ projectId: remoteRepo.id, hostId: 'ssh:ssh-1' })
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('moving one twin of a repo id on a paired server (server-local and server-SSH rows)', () => {
|
||||
it('names the twin by its server-side SSH target and updates only that row', async () => {
|
||||
const serverLocalTwin: Repo = { ...remoteRepo, connectionId: null, projectGroupId: null }
|
||||
const serverSshTwin: Repo = { ...remoteRepo, connectionId: 'srv-ssh', projectGroupId: null }
|
||||
runtimeMoveReturns({ ...serverSshTwin, projectGroupId: 'remote-group' })
|
||||
const store = createTestStore()
|
||||
store.setState({
|
||||
repos: [serverLocalTwin, serverSshTwin],
|
||||
projectGroups: [group('remote-group', REMOTE_HOST)]
|
||||
})
|
||||
|
||||
await expect(store.getState().moveProjectToGroup(serverSshTwin, 'remote-group')).resolves.toBe(
|
||||
true
|
||||
)
|
||||
|
||||
expect(moveCalls().map((request) => request.params)).toEqual([
|
||||
{ repo: remoteRepo.id, groupId: 'remote-group', connectionId: 'srv-ssh' }
|
||||
])
|
||||
expect(store.getState().repos.map((repo) => repo.projectGroupId ?? null)).toEqual([
|
||||
null,
|
||||
'remote-group'
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -849,9 +849,9 @@ describe('project group store routing', () => {
|
||||
const store = createTestStore()
|
||||
store.setState({ repos: [remoteRepo], projectGroups: [projectGroup] })
|
||||
|
||||
await expect(
|
||||
store.getState().moveProjectToGroup(remoteRepo.id, projectGroup.id, 3)
|
||||
).resolves.toBe(true)
|
||||
await expect(store.getState().moveProjectToGroup(remoteRepo, projectGroup.id, 3)).resolves.toBe(
|
||||
true
|
||||
)
|
||||
|
||||
expect(projectGroupsMoveProject).toHaveBeenCalledWith({
|
||||
projectId: remoteRepo.id,
|
||||
|
||||
@@ -589,7 +589,11 @@ describe('repo slice runtime routing', () => {
|
||||
repos: [{ ...remoteRepo, executionHostId: 'runtime:env-1' }]
|
||||
})
|
||||
|
||||
await expect(store.getState().moveProjectToGroup(remoteRepo.id, 'group-1')).resolves.toBe(true)
|
||||
await expect(
|
||||
store
|
||||
.getState()
|
||||
.moveProjectToGroup({ ...remoteRepo, executionHostId: 'runtime:env-1' }, 'group-1')
|
||||
).resolves.toBe(true)
|
||||
|
||||
expect(store.getState().repos).toEqual([
|
||||
{ ...remoteRepo, projectGroupId: 'group-1', executionHostId: 'runtime:env-1' }
|
||||
@@ -597,7 +601,7 @@ describe('repo slice runtime routing', () => {
|
||||
expect(runtimeEnvironmentCall).toHaveBeenCalledWith({
|
||||
selector: 'env-1',
|
||||
method: 'projectGroup.moveProject',
|
||||
params: { repo: remoteRepo.id, groupId: 'group-1', order: undefined },
|
||||
params: { repo: remoteRepo.id, groupId: 'group-1', order: undefined, connectionId: null },
|
||||
timeoutMs: 15_000
|
||||
})
|
||||
expect(projectGroupsMoveProject).not.toHaveBeenCalled()
|
||||
|
||||
@@ -62,10 +62,12 @@ export const ProjectGroupSelector = z.object({
|
||||
groupId: requiredString('Missing group id')
|
||||
})
|
||||
|
||||
// Why `connectionId` is optional: older clients omit it, and older hosts strip it (bare selector).
|
||||
export const ProjectGroupMoveProject = z.object({
|
||||
repo: requiredString('Missing repo selector'),
|
||||
groupId: OptionalString.nullable(),
|
||||
order: OptionalFiniteNumber
|
||||
order: OptionalFiniteNumber,
|
||||
connectionId: z.string().nullable().optional()
|
||||
})
|
||||
|
||||
export const ProjectGroupScanNested = z.object({
|
||||
|
||||
@@ -126,7 +126,11 @@ for (const delayCreateResponse of [false, true]) {
|
||||
async ({ repoIds, groupId }) => {
|
||||
const store = window.__store!
|
||||
for (const repoId of repoIds.slice(0, 2)) {
|
||||
await store.getState().moveProjectToGroup(repoId, groupId)
|
||||
const repo = store.getState().repos.find((candidate) => candidate.id === repoId)
|
||||
if (!repo) {
|
||||
throw new Error(`Missing repo ${repoId}`)
|
||||
}
|
||||
await store.getState().moveProjectToGroup(repo, groupId)
|
||||
}
|
||||
const collapsedGroups = store
|
||||
.getState()
|
||||
|
||||
@@ -127,7 +127,7 @@ test('keeps a collapsed group visible when an idle card below it grows', async (
|
||||
if (!group) {
|
||||
throw new Error('Fixture group was not created')
|
||||
}
|
||||
await store.getState().moveProjectToGroup(member.id, group.id)
|
||||
await store.getState().moveProjectToGroup(member, group.id)
|
||||
const state = store.getState()
|
||||
state.setGroupBy('repo')
|
||||
state.setProjectOrderBy('manual')
|
||||
|
||||
@@ -31,7 +31,11 @@ test('locally controlled project-group modal keeps child-toggle input', async ({
|
||||
if (!group) {
|
||||
throw new Error('Missing project group')
|
||||
}
|
||||
await store.getState().moveProjectToGroup(parent.repoId, group.id)
|
||||
const parentRepo = store.getState().repos.find((repo) => repo.id === parent.repoId)
|
||||
if (!parentRepo) {
|
||||
throw new Error('Missing parent repo')
|
||||
}
|
||||
await store.getState().moveProjectToGroup(parentRepo, group.id)
|
||||
store.getState().setGroupBy('repo')
|
||||
await store.getState().setKeybindingOverride('sidebar.childWorkspaces.toggle', ['Mod+Alt+H'])
|
||||
window.addEventListener('keydown', (event) => {
|
||||
|
||||
Reference in New Issue
Block a user