fix(client): preserve sidebar drag ordering
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user