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]
|
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. */
|
/** Reconcile a stored view order with the Workspace's current session account. */
|
||||||
function reconciledSessionOrder(sessionIds: readonly SessionId[], stored: readonly string[] | undefined): SessionId[] {
|
function reconciledSessionOrder(sessionIds: readonly SessionId[], stored: readonly string[] | undefined): SessionId[] {
|
||||||
if (stored === undefined) return [...sessionIds]
|
if (stored === undefined) return [...sessionIds]
|
||||||
@@ -241,23 +263,7 @@ function SessionTree({
|
|||||||
const workspaceDropCommitted = useRef(false)
|
const workspaceDropCommitted = useRef(false)
|
||||||
const previousOrderBy = useRef(orderBy)
|
const previousOrderBy = useRef(orderBy)
|
||||||
const nativeDragActive = drag !== null || workspaceDrag !== null
|
const nativeDragActive = drag !== null || workspaceDrag !== null
|
||||||
useEffect(() => {
|
useNativeDragAcceptance(nativeDragActive)
|
||||||
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])
|
|
||||||
const currentGroup = current === undefined
|
const currentGroup = current === undefined
|
||||||
? undefined
|
? undefined
|
||||||
: (workspaces.find(w => w.sessionIds.includes(current))?.workspaceId as string | undefined)
|
: (workspaces.find(w => w.sessionIds.includes(current))?.workspaceId as string | undefined)
|
||||||
@@ -590,20 +596,7 @@ function FlatList({
|
|||||||
}, [baseRows, recentSessionOrder, sessionIds])
|
}, [baseRows, recentSessionOrder, sessionIds])
|
||||||
const [drag, setDrag] = useState<DragState | null>(null)
|
const [drag, setDrag] = useState<DragState | null>(null)
|
||||||
const dropCommitted = useRef(false)
|
const dropCommitted = useRef(false)
|
||||||
useEffect(() => {
|
useNativeDragAcceptance(drag !== null)
|
||||||
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])
|
|
||||||
const commitDrag = (activeDrag: DragState, over: NonNullable<DragState['over']>): void => {
|
const commitDrag = (activeDrag: DragState, over: NonNullable<DragState['over']>): void => {
|
||||||
if (dropCommitted.current) return
|
if (dropCommitted.current) return
|
||||||
dropCommitted.current = true
|
dropCommitted.current = true
|
||||||
|
|||||||
@@ -259,6 +259,18 @@ function sessionStatuses(
|
|||||||
return [{ state: 'done', label: t('status.idle') }]
|
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. */
|
/** Hover-card body: full title, relative time, and every relevant live status. */
|
||||||
function SessionHoverContent({ node, now, t }: { node: SessionNode; now: number; t: RowTranslate }) {
|
function SessionHoverContent({ node, now, t }: { node: SessionNode; now: number; t: RowTranslate }) {
|
||||||
const statuses = sessionStatuses(node, t)
|
const statuses = sessionStatuses(node, t)
|
||||||
@@ -308,12 +320,7 @@ export function SearchResultItem({ result, currentId, onOpen, t }: {
|
|||||||
<span className={css.searchResultHeading}>
|
<span className={css.searchResultHeading}>
|
||||||
<span className={css.slot}>
|
<span className={css.slot}>
|
||||||
{(primaryStatus.state !== 'done' || result.completed) && (
|
{(primaryStatus.state !== 'done' || result.completed) && (
|
||||||
<>
|
<SessionStatusDots statuses={statuses} />
|
||||||
<StateDot state={primaryStatus.state} />
|
|
||||||
{statuses.map(status => (
|
|
||||||
<span className={css.visuallyHidden} key={status.label}>{status.label}</span>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
<span className={css.searchResultTitle}>{result.title}</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. */}
|
and is cleared by opening the session. */}
|
||||||
{(!flat || showStatus) && (
|
{(!flat || showStatus) && (
|
||||||
<span className={css.slot}>
|
<span className={css.slot}>
|
||||||
{showStatus && (
|
{showStatus && <SessionStatusDots statuses={statuses} />}
|
||||||
<>
|
|
||||||
<StateDot state={primaryStatus.state} />
|
|
||||||
{statuses.map(status => (
|
|
||||||
<span className={css.visuallyHidden} key={status.label}>{status.label}</span>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<span className={css.title}>{title}</span>
|
<span className={css.title}>{title}</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user