Tom Butcher d6c72c21e8
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
Refactor Dashboard Navigation and Tabs for Enhanced User Experience
- Restructured the DashboardNavigation component to separate leading and trailing navigation elements, improving layout and clarity.
- Updated CSS styles for navigation and dashboard tabs to enhance responsiveness and visual consistency.
- Implemented drag-and-drop functionality for dashboard tabs, allowing users to reorder tabs seamlessly.
- Introduced new styles for tab items and improved handling of drag states, enhancing the overall interaction experience.
2026-09-17 23:36:44 +01:00

330 lines
9.6 KiB
JavaScript

import PropTypes from 'prop-types'
import { useCallback, useState } from 'react'
import { Button, Flex, Typography } from 'antd'
import classNames from 'classnames'
import ScrollBox from './ScrollBox'
import PlusIcon from '../../Icons/PlusIcon'
import XMarkIcon from '../../Icons/XMarkIcon'
import HomeIcon from '../../Icons/HomeIcon'
import { getModelByName } from '../../../database/ObjectModels'
import { useNavigationTabs } from '../context/NavigationTabsContext'
import { getDesktopWindowId } from '../../../electrobun-bridge.js'
import { hasExternalTabDrag, writeTabDragData } from './tabDrag'
const { Text } = Typography
const DashboardTabChrome = () => (
<>
<span className='dashboard-tab-item-outline-container' aria-hidden='true'>
<span className='dashboard-tab-item-outline' aria-hidden='true' />
</span>
<span
className='dashboard-tab-item-corner dashboard-tab-item-corner-left'
aria-hidden='true'
>
<span className='dashboard-tab-item-corner-inner' />
</span>
<span
className='dashboard-tab-item-corner dashboard-tab-item-corner-right'
aria-hidden='true'
>
<span className='dashboard-tab-item-corner-inner' />
</span>
</>
)
const DashboardTabLabel = ({ tab, onClose }) => (
<Flex align='center' gap={6} className='dashboard-tabs-label'>
<Text ellipsis style={{ maxWidth: 240 }}>
{tab.title || 'Farm Control'}
</Text>
<span
className='dashboard-tabs-close'
role='button'
tabIndex={-1}
onClick={(event) => {
event.preventDefault()
event.stopPropagation()
onClose?.(tab.id)
}}
onMouseDown={(event) => event.stopPropagation()}
>
<XMarkIcon style={{ fontSize: 10 }} />
</span>
</Flex>
)
DashboardTabLabel.propTypes = {
tab: PropTypes.shape({
id: PropTypes.string.isRequired,
title: PropTypes.string
}).isRequired,
onClose: PropTypes.func
}
const DashboardTabs = () => {
const {
tabs,
activeTabId,
selectTab,
addTab,
closeTab,
reorderTabs,
handleTabDragStart,
handleTabDragEnd,
handleExternalTabDrop
} = useNavigationTabs()
const [draggedValue, setDraggedValue] = useState(null)
const [dropTarget, setDropTarget] = useState(null)
const clearDragState = useCallback(() => {
setDraggedValue(null)
setDropTarget(null)
}, [])
const handleItemDragStart = (event, tabId) => {
const tab = tabs.find((item) => item.id === tabId)
if (!tab) return
event.stopPropagation()
const item = event.currentTarget
if (item) {
const rect = item.getBoundingClientRect()
event.dataTransfer.setDragImage(
item,
event.clientX - rect.left,
event.clientY - rect.top
)
}
setDraggedValue(tabId)
event.dataTransfer.effectAllowed = 'move'
writeTabDragData(event, {
tab,
sourceWindowId: getDesktopWindowId()
})
handleTabDragStart(tabId)
}
const handleItemDragEnd = () => {
handleTabDragEnd()
clearDragState()
}
const handleItemDragOver = (event, tab) => {
const isExternal = hasExternalTabDrag(event)
if (draggedValue == null && !isExternal) return
if (draggedValue != null && String(tab.id) === String(draggedValue)) return
event.preventDefault()
event.stopPropagation()
event.dataTransfer.dropEffect = 'move'
const rect = event.currentTarget.getBoundingClientRect()
setDropTarget({
value: tab.id,
insertBefore: event.clientX < rect.left + rect.width / 2
})
}
const handleItemDrop = (event, tab) => {
const isExternal = draggedValue == null && hasExternalTabDrag(event)
event.preventDefault()
event.stopPropagation()
const target = dropTarget || {
value: tab.id,
insertBefore: true
}
if (isExternal) {
void handleExternalTabDrop(target.value, target.insertBefore)
clearDragState()
return
}
if (draggedValue == null) return
if (String(draggedValue) !== String(target.value)) {
reorderTabs(draggedValue, target.value, target.insertBefore)
}
clearDragState()
}
const handleListDragOver = (event) => {
const isExternal = hasExternalTabDrag(event)
if (draggedValue == null && !isExternal) return
if (
event.target instanceof Element &&
event.target.closest('.dashboard-tab-item')
) {
return
}
event.preventDefault()
event.dataTransfer.dropEffect = 'move'
const lastTab = tabs[tabs.length - 1]
if (!lastTab) return
setDropTarget({
value: lastTab.id,
insertBefore: false
})
}
const handleListDrop = (event) => {
const isExternal = draggedValue == null && hasExternalTabDrag(event)
if (draggedValue == null && !isExternal) return
if (
event.target instanceof Element &&
event.target.closest('.dashboard-tab-item')
) {
return
}
event.preventDefault()
event.stopPropagation()
const lastTab = tabs[tabs.length - 1]
if (!lastTab) {
clearDragState()
return
}
if (isExternal) {
void handleExternalTabDrop(lastTab.id, false)
clearDragState()
return
}
if (String(draggedValue) !== String(lastTab.id)) {
reorderTabs(draggedValue, lastTab.id, false)
}
clearDragState()
}
const handleStripDragOver = (event) => {
if (!hasExternalTabDrag(event)) return
event.preventDefault()
event.dataTransfer.dropEffect = 'move'
}
const handleStripDrop = (event) => {
if (!hasExternalTabDrag(event)) return
event.preventDefault()
const lastTab = tabs[tabs.length - 1]
void handleExternalTabDrop(lastTab?.id, false)
}
const handleKeyDown = (event, tabIndex) => {
if (event.key !== 'ArrowRight' && event.key !== 'ArrowLeft') return
event.preventDefault()
const direction = event.key === 'ArrowRight' ? 1 : -1
const currentIndex = tabs.findIndex((tab) => tab.id === activeTabId)
const startIndex = currentIndex === -1 ? tabIndex : currentIndex
const nextIndex = (startIndex + direction + tabs.length) % tabs.length
selectTab(tabs[nextIndex].id)
}
const isReordering = draggedValue != null || dropTarget != null
return (
<Flex
align='flex-start'
gap={4}
className={classNames(
'dashboard-tabs',
'electrobun-webkit-app-region-drag'
)}
style={{ flex: 1, minWidth: 0 }}
onDragOver={handleStripDragOver}
onDrop={handleStripDrop}
>
<ScrollBox
className='dashboard-tabs-wrap'
horizontal
horizontalBottomPadding={0}
>
<Flex align='flex-start' gap={8} className='dashboard-tabs-inner'>
<div
className={classNames('dashboard-tabs-list', {
'dashboard-tabs-reordering': isReordering
})}
role='tablist'
onDragOver={handleListDragOver}
onDrop={handleListDrop}
>
{tabs.map((tab, index) => {
const model = tab.modelName ? getModelByName(tab.modelName) : null
const Icon = model?.icon || HomeIcon
const isSelected = tab.id === activeTabId
const isDragging =
draggedValue != null && String(draggedValue) === String(tab.id)
const isDropTarget =
dropTarget != null &&
String(dropTarget.value) === String(tab.id) &&
String(draggedValue) !== String(tab.id)
return (
<div
key={tab.id}
className={classNames('dashboard-tab-item-container', {
'dashboard-tab-item-container-active': isSelected
})}
>
<button
type='button'
role='tab'
aria-selected={isSelected}
tabIndex={isSelected ? 0 : -1}
className={classNames(
'dashboard-tab-item',
'electrobun-webkit-app-region-no-drag',
{
'dashboard-tab-item-active': isSelected,
'dashboard-tab-item-dragging': isDragging,
'dashboard-tab-item-drop-before':
isDropTarget && dropTarget.insertBefore,
'dashboard-tab-item-drop-after':
isDropTarget && !dropTarget.insertBefore
}
)}
draggable
onDragStart={(event) => handleItemDragStart(event, tab.id)}
onDragEnd={handleItemDragEnd}
onClick={() => selectTab(tab.id)}
onKeyDown={(event) => handleKeyDown(event, index)}
onDragOver={(event) => handleItemDragOver(event, tab)}
onDrop={(event) => handleItemDrop(event, tab)}
>
<DashboardTabChrome />
<span className='dashboard-tab-item-icon'>
<Icon
style={{ fontSize: 14, marginTop: '-2px' }}
color='secondary'
/>
</span>
<DashboardTabLabel tab={tab} onClose={closeTab} />
</button>
</div>
)
})}
</div>
<Button
type='text'
size='small'
className='dashboard-tabs-add-button electrobun-webkit-app-region-no-drag'
onClick={addTab}
icon={<PlusIcon style={{ fontSize: 14, marginTop: 3 }} />}
/>
</Flex>
</ScrollBox>
</Flex>
)
}
export default DashboardTabs