All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
- 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.
330 lines
9.6 KiB
JavaScript
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
|