fix(client): preserve sidebar drag ordering

This commit is contained in:
_Kerman
2026-08-11 16:49:21 +08:00
parent 5b1da441d5
commit fcbe3ac836
7 changed files with 76 additions and 26 deletions

View File

@@ -299,7 +299,9 @@
margin-left: -4px;
margin-right: calc(-1 * var(--dsh-session-list-edge-inset));
padding-left: 4px;
overflow: hidden;
/* The list remains the scroll clip. This seat stays visible so the
absolutely positioned first-boundary marker can occupy the header gap. */
overflow: visible;
}
.rail .listArea {
@@ -387,6 +389,7 @@
margin-top: 4px;
}
.listTopDropIndicator,
.workspaceDropBefore::before,
.workspaceDropAfter::after {
content: '';
@@ -408,6 +411,18 @@
pointer-events: none;
}
/* The first insertion boundary keeps the same -8px coordinate as every
Workspace boundary, but lives outside the scrolling clip. */
.listTopDropIndicator {
top: -8px;
left: -4px;
right: calc(var(--dsh-session-list-edge-inset) + 4px);
}
.listTopDropActive > .workspaceDropBefore:first-child::before {
display: none;
}
.workspaceDropBefore::before {
top: -8px;
}

View File

@@ -299,15 +299,13 @@ function SessionTree({
? group.sessions.length
: group.sessions.findIndex(session => session.id === anchor)
if (sourceIndex !== -1 && (anchorIndex === sourceIndex || anchorIndex === sourceIndex + 1)) return
if (orderBy === 'updated') {
const account = orderedWorkspaces.find(workspace => workspace.workspaceId === activeDrag.workspaceId)
if (account === undefined) return
const nextOrder = account.sessionIds.filter(id => id !== activeDrag.sessionId)
const insertAt = anchor === undefined ? nextOrder.length : nextOrder.indexOf(anchor)
nextOrder.splice(insertAt === -1 ? nextOrder.length : insertAt, 0, activeDrag.sessionId)
setRecentSessionOrder(activeDrag.workspaceId, nextOrder.map(id => id as string))
return
}
const account = orderedWorkspaces.find(workspace => workspace.workspaceId === activeDrag.workspaceId)
if (account === undefined) return
const nextOrder = account.sessionIds.filter(id => id !== activeDrag.sessionId)
const insertAt = anchor === undefined ? nextOrder.length : nextOrder.indexOf(anchor)
nextOrder.splice(insertAt === -1 ? nextOrder.length : insertAt, 0, activeDrag.sessionId)
setRecentSessionOrder(activeDrag.workspaceId, nextOrder.map(id => id as string))
if (orderBy === 'updated') return
insertSessionBefore(activeDrag.workspaceId, activeDrag.sessionId, anchor).catch((reason: unknown) => {
console.warn('session reorder rejected:', reason)
})
@@ -332,10 +330,18 @@ function SessionTree({
console.warn('workspace reorder rejected:', reason)
})
}
const workspaceDropAtListStart = groups[0]?.workspaceId !== undefined
&& workspaceDrag?.over?.id === groups[0].workspaceId
&& workspaceDrag.over.half === 'before'
return (
<div className={clsx(css.treeBody, css.wide)}>
<div className={css.list} role="tree" aria-label={t('section.sessions')}>
{workspaceDropAtListStart && <span className={css.listTopDropIndicator} aria-hidden="true" />}
<div
className={clsx(css.list, workspaceDropAtListStart && css.listTopDropActive)}
role="tree"
aria-label={t('section.sessions')}
>
{groups.length === 0 && (
<div className={css.empty}>{t('empty.none')}</div>
)}

View File

@@ -77,11 +77,14 @@ describe('WorkspaceBrowser.module.css list', () => {
})
it('draws drag targets as a hollow leading dot joined to the insertion line', () => {
const listTopMarker = declarations('.listTopDropIndicator')
const workspaceMarker = declarations('.workspaceDropBefore::before')
const sessionMarker = rowDeclarations('.sessionRow.dropBefore::before')
expect(listTopMarker?.get('top')).toBe('-8px')
expect(listTopMarker?.get('left')).toBe('-4px')
expect(workspaceMarker?.get('left')).toBe('-4px')
expect(sessionMarker?.get('left')).toBe('0')
for (const marker of [workspaceMarker, sessionMarker]) {
for (const marker of [listTopMarker, workspaceMarker, sessionMarker]) {
expect(marker?.get('height')).toBe('12px')
expect(marker?.get('background')).toContain('radial-gradient')
expect(marker?.get('background')).toContain('linear-gradient')

View File

@@ -661,6 +661,28 @@ describe('WorkspaceBrowser', () => {
expect(insertWorkspaceBefore).toHaveBeenCalledWith(wid('tail'), wid('beta'))
})
it('draws the first Workspace insertion boundary on the scroll container', () => {
mount({
useWorkspaces: hook(workspaceState([
workspace('alpha', []),
workspace('beta', []),
])),
})
const source = screen.getByText('beta').closest('[role="treeitem"]') as HTMLElement
let firstSection = screen.getByText('alpha').closest('[role="treeitem"]')?.parentElement as HTMLElement
while (firstSection.parentElement?.getAttribute('role') !== 'tree') {
firstSection = firstSection.parentElement as HTMLElement
}
firstSection.getBoundingClientRect = () => ({
top: 100, bottom: 134, left: 0, right: 200, width: 200, height: 34, x: 0, y: 100, toJSON: () => ({}),
})
fireEvent.dragStart(source, { dataTransfer: dragData() })
fireDrag(firstSection, 'dragOver', 105)
expect(firstSection.parentElement?.className).toContain('listTopDropActive')
const marker = firstSection.parentElement?.previousElementSibling
expect(marker?.className).toContain('listTopDropIndicator')
})
it('accepts a document-level drop and commits the last Workspace marker on drag end', () => {
const insertWorkspaceBefore = vi.fn(async () => {})
mount({