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.
This commit is contained in:
@@ -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<DragState | null>(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<DragState['over']>): void => {
|
||||
if (dropCommitted.current) return
|
||||
dropCommitted.current = true
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<StateDot state={statuses[0].state} />
|
||||
{statuses.map(status => (
|
||||
<span className={css.visuallyHidden} key={status.label}>{status.label}</span>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/** 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 }: {
|
||||
<span className={css.searchResultHeading}>
|
||||
<span className={css.slot}>
|
||||
{(primaryStatus.state !== 'done' || result.completed) && (
|
||||
<>
|
||||
<StateDot state={primaryStatus.state} />
|
||||
{statuses.map(status => (
|
||||
<span className={css.visuallyHidden} key={status.label}>{status.label}</span>
|
||||
))}
|
||||
</>
|
||||
<SessionStatusDots statuses={statuses} />
|
||||
)}
|
||||
</span>
|
||||
<span className={css.searchResultTitle}>{result.title}</span>
|
||||
@@ -417,14 +424,7 @@ export function SessionNodeItem({ node, currentId, now, onOpen, onRename, onFork
|
||||
and is cleared by opening the session. */}
|
||||
{(!flat || showStatus) && (
|
||||
<span className={css.slot}>
|
||||
{showStatus && (
|
||||
<>
|
||||
<StateDot state={primaryStatus.state} />
|
||||
{statuses.map(status => (
|
||||
<span className={css.visuallyHidden} key={status.label}>{status.label}</span>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
{showStatus && <SessionStatusDots statuses={statuses} />}
|
||||
</span>
|
||||
)}
|
||||
<span className={css.title}>{title}</span>
|
||||
|
||||
Reference in New Issue
Block a user