From 64326cb597784a40c2e893800d9ef012475a141a Mon Sep 17 00:00:00 2001 From: _Kerman Date: Wed, 12 Aug 2026 12:01:23 +0800 Subject: [PATCH] refactor(ui-workspace): extract shared drag-accept and status-dot renderers The flat-list ordering change duplicated the document-level native-drag acceptance effect and the status-dot block across the search and session rows, tripping the duplication gate. Extract useNativeDragAcceptance and SessionStatusDots so both call sites share one implementation. --- .../src/client/WorkspaceBrowser.tsx | 55 ++++++++----------- .../ui-workspace/src/client/rows/Rows.tsx | 28 +++++----- 2 files changed, 38 insertions(+), 45 deletions(-) diff --git a/packages/client/ui-workspace/src/client/WorkspaceBrowser.tsx b/packages/client/ui-workspace/src/client/WorkspaceBrowser.tsx index 9478c63a55..e9d6ed192f 100644 --- a/packages/client/ui-workspace/src/client/WorkspaceBrowser.tsx +++ b/packages/client/ui-workspace/src/client/WorkspaceBrowser.tsx @@ -54,6 +54,28 @@ function toggled(list: readonly string[], key: string): string[] { return list.includes(key) ? list.filter(k => k !== key) : [...list, key] } +/** + * Accept the native drag at document level while a row drag is active: row + * hover still owns the insertion marker, and releasing outside the list must + * not be rendered as a rejected drop before dragend commits that last marker. + */ +function useNativeDragAcceptance(active: boolean): void { + useEffect(() => { + if (!active) return + const acceptDrag = (event: DragEvent): void => { + event.preventDefault() + if (event.dataTransfer !== null) event.dataTransfer.dropEffect = 'move' + } + const acceptDrop = (event: DragEvent): void => { event.preventDefault() } + document.addEventListener('dragover', acceptDrag) + document.addEventListener('drop', acceptDrop) + return () => { + document.removeEventListener('dragover', acceptDrag) + document.removeEventListener('drop', acceptDrop) + } + }, [active]) +} + /** Reconcile a stored view order with the Workspace's current session account. */ function reconciledSessionOrder(sessionIds: readonly SessionId[], stored: readonly string[] | undefined): SessionId[] { if (stored === undefined) return [...sessionIds] @@ -241,23 +263,7 @@ function SessionTree({ const workspaceDropCommitted = useRef(false) const previousOrderBy = useRef(orderBy) const nativeDragActive = drag !== null || workspaceDrag !== null - useEffect(() => { - if (!nativeDragActive) return - // Row hover still owns the insertion marker. Accept the native drag at - // document level so releasing outside the list is not rendered as a - // rejected drop before dragend commits that last marker. - const acceptDrag = (event: DragEvent): void => { - event.preventDefault() - if (event.dataTransfer !== null) event.dataTransfer.dropEffect = 'move' - } - const acceptDrop = (event: DragEvent): void => { event.preventDefault() } - document.addEventListener('dragover', acceptDrag) - document.addEventListener('drop', acceptDrop) - return () => { - document.removeEventListener('dragover', acceptDrag) - document.removeEventListener('drop', acceptDrop) - } - }, [nativeDragActive]) + useNativeDragAcceptance(nativeDragActive) const currentGroup = current === undefined ? undefined : (workspaces.find(w => w.sessionIds.includes(current))?.workspaceId as string | undefined) @@ -590,20 +596,7 @@ function FlatList({ }, [baseRows, recentSessionOrder, sessionIds]) const [drag, setDrag] = useState(null) const dropCommitted = useRef(false) - useEffect(() => { - if (drag === null) return - const acceptDrag = (event: DragEvent): void => { - event.preventDefault() - if (event.dataTransfer !== null) event.dataTransfer.dropEffect = 'move' - } - const acceptDrop = (event: DragEvent): void => { event.preventDefault() } - document.addEventListener('dragover', acceptDrag) - document.addEventListener('drop', acceptDrop) - return () => { - document.removeEventListener('dragover', acceptDrag) - document.removeEventListener('drop', acceptDrop) - } - }, [drag]) + useNativeDragAcceptance(drag !== null) const commitDrag = (activeDrag: DragState, over: NonNullable): void => { if (dropCommitted.current) return dropCommitted.current = true diff --git a/packages/client/ui-workspace/src/client/rows/Rows.tsx b/packages/client/ui-workspace/src/client/rows/Rows.tsx index 63b3068be2..481e0f0e47 100644 --- a/packages/client/ui-workspace/src/client/rows/Rows.tsx +++ b/packages/client/ui-workspace/src/client/rows/Rows.tsx @@ -259,6 +259,18 @@ function sessionStatuses( return [{ state: 'done', label: t('status.idle') }] } +/** Primary status dot plus every status's screen-reader label, shared by the search and session rows. */ +function SessionStatusDots({ statuses }: { statuses: readonly [SessionStatus, ...SessionStatus[]] }) { + return ( + <> + + {statuses.map(status => ( + {status.label} + ))} + + ) +} + /** Hover-card body: full title, relative time, and every relevant live status. */ function SessionHoverContent({ node, now, t }: { node: SessionNode; now: number; t: RowTranslate }) { const statuses = sessionStatuses(node, t) @@ -308,12 +320,7 @@ export function SearchResultItem({ result, currentId, onOpen, t }: { {(primaryStatus.state !== 'done' || result.completed) && ( - <> - - {statuses.map(status => ( - {status.label} - ))} - + )} {result.title} @@ -417,14 +424,7 @@ export function SessionNodeItem({ node, currentId, now, onOpen, onRename, onFork and is cleared by opening the session. */} {(!flat || showStatus) && ( - {showStatus && ( - <> - - {statuses.map(status => ( - {status.label} - ))} - - )} + {showStatus && } )} {title}