Improve current workspace reveal control (#2327)
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
parent
7d71131bb4
commit
288a8b423d
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,81 @@
|
|||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const revealWorktreeInSidebar = vi.fn()
|
||||
|
||||
const baseState: {
|
||||
activeWorktreeId: string | null
|
||||
revealWorktreeInSidebar: typeof revealWorktreeInSidebar
|
||||
worktreesByRepo: Record<string, ({ id: string; repoId: string } & Record<string, unknown>)[]>
|
||||
} = {
|
||||
activeWorktreeId: 'wt-1',
|
||||
revealWorktreeInSidebar,
|
||||
worktreesByRepo: {
|
||||
'repo-1': [
|
||||
{
|
||||
id: 'wt-1',
|
||||
repoId: 'repo-1',
|
||||
path: '/repo/worktrees/one',
|
||||
displayName: 'One',
|
||||
branch: 'one',
|
||||
head: 'abc',
|
||||
isBare: false,
|
||||
isMainWorktree: false,
|
||||
comment: '',
|
||||
linkedIssue: null,
|
||||
linkedPR: null,
|
||||
linkedLinearIssue: null,
|
||||
isArchived: false,
|
||||
isUnread: false,
|
||||
isPinned: false,
|
||||
sortOrder: 0,
|
||||
lastActivityAt: 0
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
vi.mock('@/store', () => ({
|
||||
useAppStore: {
|
||||
getState: () => baseState
|
||||
}
|
||||
}))
|
||||
|
||||
describe('reveal-sidebar-worktree', () => {
|
||||
beforeEach(() => {
|
||||
revealWorktreeInSidebar.mockReset()
|
||||
baseState.activeWorktreeId = 'wt-1'
|
||||
})
|
||||
|
||||
it('reveals the current workspace with the requested behavior', async () => {
|
||||
const { revealCurrentSidebarWorktree } = await import('./reveal-sidebar-worktree')
|
||||
|
||||
expect(revealCurrentSidebarWorktree({ behavior: 'smooth' })).toBe(true)
|
||||
|
||||
expect(revealWorktreeInSidebar).toHaveBeenCalledWith('wt-1', { behavior: 'smooth' })
|
||||
})
|
||||
|
||||
it('can reveal a specific workspace without changing other sidebar state', async () => {
|
||||
const { revealSidebarWorktree } = await import('./reveal-sidebar-worktree')
|
||||
|
||||
expect(revealSidebarWorktree('wt-1', { behavior: 'auto' })).toBe(true)
|
||||
|
||||
expect(revealWorktreeInSidebar).toHaveBeenCalledWith('wt-1', { behavior: 'auto' })
|
||||
})
|
||||
|
||||
it('does not reveal a missing workspace', async () => {
|
||||
const { revealSidebarWorktree } = await import('./reveal-sidebar-worktree')
|
||||
|
||||
expect(revealSidebarWorktree('missing', { behavior: 'auto' })).toBe(false)
|
||||
|
||||
expect(revealWorktreeInSidebar).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does nothing when there is no active workspace', async () => {
|
||||
const { revealCurrentSidebarWorktree } = await import('./reveal-sidebar-worktree')
|
||||
|
||||
baseState.activeWorktreeId = null
|
||||
expect(revealCurrentSidebarWorktree({ behavior: 'smooth' })).toBe(false)
|
||||
|
||||
expect(revealWorktreeInSidebar).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import { useAppStore } from '@/store'
|
||||
import { findWorktreeById } from '@/store/slices/worktree-helpers'
|
||||
|
||||
export function revealSidebarWorktree(
|
||||
worktreeId: string,
|
||||
options?: { behavior?: 'auto' | 'smooth' }
|
||||
): boolean {
|
||||
const state = useAppStore.getState()
|
||||
const worktree = findWorktreeById(state.worktreesByRepo, worktreeId)
|
||||
if (!worktree) {
|
||||
return false
|
||||
}
|
||||
|
||||
state.revealWorktreeInSidebar(worktreeId, options)
|
||||
return true
|
||||
}
|
||||
|
||||
export function revealCurrentSidebarWorktree(options?: { behavior?: 'auto' | 'smooth' }): boolean {
|
||||
const activeWorktreeId = useAppStore.getState().activeWorktreeId
|
||||
if (!activeWorktreeId) {
|
||||
return false
|
||||
}
|
||||
return revealSidebarWorktree(activeWorktreeId, options)
|
||||
}
|
||||
|
|
@ -1,5 +1,11 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import { shouldAdjustWorktreeSidebarMeasuredRowScroll } from './WorktreeList'
|
||||
import {
|
||||
shouldConsumeStartupRevealForPendingReveal,
|
||||
resolvePendingSidebarReveal,
|
||||
shouldAdjustWorktreeSidebarMeasuredRowScroll,
|
||||
shouldShowFloatingCurrentWorkspaceButton,
|
||||
shouldQueueStartupSidebarReveal
|
||||
} from './WorktreeList'
|
||||
|
||||
describe('shouldAdjustWorktreeSidebarMeasuredRowScroll', () => {
|
||||
it('suppresses measured-row scroll correction while TanStack is scrolling', () => {
|
||||
|
|
@ -31,4 +37,157 @@ describe('shouldAdjustWorktreeSidebarMeasuredRowScroll', () => {
|
|||
})
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('queues a startup reveal once the active workspace and rows are ready', () => {
|
||||
expect(
|
||||
shouldQueueStartupSidebarReveal({
|
||||
hasQueuedStartupReveal: false,
|
||||
workspaceSessionReady: true,
|
||||
persistedUIReady: true,
|
||||
activeWorktreeId: 'wt-1',
|
||||
pendingRevealWorktree: null,
|
||||
renderRowCount: 3
|
||||
})
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('does not queue a startup reveal when another reveal is already pending', () => {
|
||||
expect(
|
||||
shouldQueueStartupSidebarReveal({
|
||||
hasQueuedStartupReveal: false,
|
||||
workspaceSessionReady: true,
|
||||
persistedUIReady: true,
|
||||
activeWorktreeId: 'wt-1',
|
||||
pendingRevealWorktree: { worktreeId: 'wt-2', behavior: 'smooth' },
|
||||
renderRowCount: 3
|
||||
})
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('consumes startup reveal when an explicit reveal is already pending', () => {
|
||||
expect(
|
||||
shouldConsumeStartupRevealForPendingReveal({
|
||||
hasQueuedStartupReveal: false,
|
||||
workspaceSessionReady: true,
|
||||
persistedUIReady: true,
|
||||
pendingRevealWorktree: { worktreeId: 'wt-2', behavior: 'smooth' }
|
||||
})
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('does not consume startup reveal before hydration is ready', () => {
|
||||
expect(
|
||||
shouldConsumeStartupRevealForPendingReveal({
|
||||
hasQueuedStartupReveal: false,
|
||||
workspaceSessionReady: true,
|
||||
persistedUIReady: false,
|
||||
pendingRevealWorktree: { worktreeId: 'wt-2', behavior: 'smooth' }
|
||||
})
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('does not queue a startup reveal twice', () => {
|
||||
expect(
|
||||
shouldQueueStartupSidebarReveal({
|
||||
hasQueuedStartupReveal: true,
|
||||
workspaceSessionReady: true,
|
||||
persistedUIReady: true,
|
||||
activeWorktreeId: 'wt-1',
|
||||
pendingRevealWorktree: null,
|
||||
renderRowCount: 3
|
||||
})
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('does not queue a startup reveal before hydration is ready', () => {
|
||||
expect(
|
||||
shouldQueueStartupSidebarReveal({
|
||||
hasQueuedStartupReveal: false,
|
||||
workspaceSessionReady: false,
|
||||
persistedUIReady: true,
|
||||
activeWorktreeId: 'wt-1',
|
||||
pendingRevealWorktree: null,
|
||||
renderRowCount: 3
|
||||
})
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('keeps pending reveal requests when the worktree still exists but the row is unresolved', () => {
|
||||
expect(
|
||||
resolvePendingSidebarReveal({
|
||||
targetIndex: -1,
|
||||
targetWorktreeStillExists: true
|
||||
})
|
||||
).toBe('keep-pending')
|
||||
})
|
||||
|
||||
it('clears pending reveal requests once the target disappears', () => {
|
||||
expect(
|
||||
resolvePendingSidebarReveal({
|
||||
targetIndex: -1,
|
||||
targetWorktreeStillExists: false
|
||||
})
|
||||
).toBe('clear')
|
||||
})
|
||||
|
||||
it('scrolls and clears once the target row is resolvable', () => {
|
||||
expect(
|
||||
resolvePendingSidebarReveal({
|
||||
targetIndex: 4,
|
||||
targetWorktreeStillExists: true
|
||||
})
|
||||
).toBe('scroll-and-clear')
|
||||
})
|
||||
|
||||
it('shows the floating reveal action when the current workspace row is hidden', () => {
|
||||
expect(
|
||||
shouldShowFloatingCurrentWorkspaceButton({
|
||||
currentWorktreeId: 'wt-1',
|
||||
currentRowIndex: -1,
|
||||
currentItem: null,
|
||||
scrollTop: 0,
|
||||
viewportHeight: 400,
|
||||
pendingRevealWorktreeId: null
|
||||
})
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('shows the floating reveal action when the current workspace is outside the scrollport', () => {
|
||||
expect(
|
||||
shouldShowFloatingCurrentWorkspaceButton({
|
||||
currentWorktreeId: 'wt-1',
|
||||
currentRowIndex: 10,
|
||||
currentItem: { start: 500, end: 560 },
|
||||
scrollTop: 0,
|
||||
viewportHeight: 400,
|
||||
pendingRevealWorktreeId: null
|
||||
})
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('hides the floating reveal action when the current workspace is visible', () => {
|
||||
expect(
|
||||
shouldShowFloatingCurrentWorkspaceButton({
|
||||
currentWorktreeId: 'wt-1',
|
||||
currentRowIndex: 3,
|
||||
currentItem: { start: 120, end: 180 },
|
||||
scrollTop: 100,
|
||||
viewportHeight: 200,
|
||||
pendingRevealWorktreeId: null
|
||||
})
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('hides the floating reveal action while the current workspace reveal is pending', () => {
|
||||
expect(
|
||||
shouldShowFloatingCurrentWorkspaceButton({
|
||||
currentWorktreeId: 'wt-1',
|
||||
currentRowIndex: -1,
|
||||
currentItem: null,
|
||||
scrollTop: 0,
|
||||
viewportHeight: 400,
|
||||
pendingRevealWorktreeId: 'wt-1'
|
||||
})
|
||||
).toBe(false)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -47,6 +47,11 @@ import { DEFAULT_PET_ID, isBundledPetId } from '../../components/pet/pet-models'
|
|||
import { revokeCustomPetBlobUrl } from '../../components/pet/pet-blob-cache'
|
||||
import { isGitRepoKind } from '../../../../shared/repo-kind'
|
||||
|
||||
export type PendingSidebarWorktreeReveal = {
|
||||
worktreeId: string
|
||||
behavior: 'auto' | 'smooth'
|
||||
}
|
||||
|
||||
function clampPetSize(size: number): number {
|
||||
if (!Number.isFinite(size)) {
|
||||
return PET_SIZE_DEFAULT
|
||||
|
|
@ -408,8 +413,11 @@ export type UISlice = {
|
|||
* problem. */
|
||||
petSize: number
|
||||
setPetSize: (size: number) => void
|
||||
pendingRevealWorktreeId: string | null
|
||||
revealWorktreeInSidebar: (worktreeId: string) => void
|
||||
pendingRevealWorktree: PendingSidebarWorktreeReveal | null
|
||||
revealWorktreeInSidebar: (
|
||||
worktreeId: string,
|
||||
options?: { behavior?: PendingSidebarWorktreeReveal['behavior'] }
|
||||
) => void
|
||||
clearPendingRevealWorktreeId: () => void
|
||||
// Why: lets the SourceControl sidebar request that the diff editor scroll
|
||||
// to a specific note. Cleared by the diff decorator after it reveals the
|
||||
|
|
@ -924,9 +932,15 @@ export const createUISlice: StateCreator<AppState, [], [], UISlice> = (set, get)
|
|||
return partial
|
||||
}),
|
||||
|
||||
pendingRevealWorktreeId: null,
|
||||
revealWorktreeInSidebar: (worktreeId) => set({ pendingRevealWorktreeId: worktreeId }),
|
||||
clearPendingRevealWorktreeId: () => set({ pendingRevealWorktreeId: null }),
|
||||
pendingRevealWorktree: null,
|
||||
revealWorktreeInSidebar: (worktreeId, options) =>
|
||||
set({
|
||||
pendingRevealWorktree: {
|
||||
worktreeId,
|
||||
behavior: options?.behavior ?? 'smooth'
|
||||
}
|
||||
}),
|
||||
clearPendingRevealWorktreeId: () => set({ pendingRevealWorktree: null }),
|
||||
scrollToDiffCommentId: null,
|
||||
setScrollToDiffCommentId: (id) => set({ scrollToDiffCommentId: id }),
|
||||
persistedUIReady: false,
|
||||
|
|
|
|||
Loading…
Reference in New Issue