Compare commits

..

2 Commits

Author SHA1 Message Date
d6c72c21e8 Refactor Dashboard Navigation and Tabs for Enhanced User Experience
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.
2026-09-17 23:36:44 +01:00
7fef25d911 Enhance Dashboard Navigation with Object Navigation Hook
- Introduced `useObjectNavigationTabPage` hook to streamline navigation tab management for various dashboard components, improving consistency in object handling.
- Updated multiple components (DocumentTemplateDesign, EmailTemplateDesign, NoteInfo, GCodeFilePreview, ControlPrinter) to utilize the new hook for better object navigation and state management.
- Adjusted CSS styles for dashboard tabs to improve layout and responsiveness, enhancing user experience across devices.
2026-09-17 21:39:04 +01:00
13 changed files with 687 additions and 128 deletions

View File

@ -112,6 +112,18 @@
--webkit-user-select: none;
}
.electron-navigation-leading,
.electron-navigation-trailing {
align-self: stretch;
height: 42px;
border-bottom: 1px solid var(--color-header-border);
}
.electron-navigation-trailing {
flex: 0 1 auto;
justify-content: flex-end;
}
.electron-body * {
-webkit-user-select: none;
-khtml-user-select: none;
@ -145,12 +157,20 @@ html.macos-vibrancy .main-layout {
background: transparent !important;
}
html.macos-vibrancy .light-mode {
--color-layout-background: rgba(255, 255, 255, 0.88);
}
html.macos-vibrancy .dark-mode {
--color-layout-background: rgba(0, 0, 0, 0.88);
}
html.macos-vibrancy .light-mode .ant-layout.main-content-layout {
background: rgba(255, 255, 255, 0.88) !important;
background: var(--color-layout-background) !important;
}
html.macos-vibrancy .dark-mode .ant-layout.main-content-layout {
background: rgba(0, 0, 0, 0.88) !important;
background: var(--color-layout-background) !important;
}
html.macos-vibrancy .light-mode .ant-layout-sider {
@ -367,6 +387,7 @@ code {
:root,
.light-mode,
html:has(.light-mode) {
--color-layout-background: #ffffff;
--kbd-color: #363636;
--kbd-bg: #efefef;
--kbd-inset: #e8e8e8;
@ -378,6 +399,7 @@ html:has(.light-mode) {
.dark-mode,
html:has(.dark-mode) {
--color-layout-background: #000000;
--kbd-color: #ececec;
--kbd-bg: #3a3a3a;
--kbd-inset: #323232;
@ -3349,29 +3371,192 @@ body.objectKanbanColumnResizing * {
.dashboard-tabs {
flex: 1;
min-width: 0;
align-self: stretch;
}
.dashboard-tabs .segmented-nav-item-label {
padding-inline: 0px;
padding-left: 8px;
}
.dashboard-tabs-segmented-wrap {
.dashboard-tabs-wrap {
flex: 1;
min-width: 0;
height: 100%;
}
.dashboard-tabs-segmented-wrap .simplebar-track.simplebar-vertical {
.dashboard-tabs-wrap .simplebar-track.simplebar-vertical {
display: none;
}
.dashboard-tabs-segmented-inner {
.dashboard-tabs-inner {
width: max-content;
height: 100%;
align-items: flex-start;
box-sizing: border-box;
}
.dashboard-tabs-list {
position: relative;
display: flex;
align-items: flex-start;
gap: 4px;
padding-inline: 12px;
height: 100%;
max-height: 42px;
}
.dashboard-tab-item-container {
height: 43px;
position: relative;
}
.dashboard-tab-item-container-active {
border-bottom: none;
height: 43px;
}
.dashboard-tab-item-container:before {
content: '';
position: absolute;
top: 0;
left: -2px;
right: -2px;
bottom: 0;
border-bottom: 1px solid var(--color-header-border);
}
.dashboard-tab-item-container-active + .dashboard-tab-item-container:before {
left: 16px;
}
.dashboard-tabs-list:has(+ .dashboard-tab-item-container-active):before {
right: 9.5px;
}
.dashboard-tab-item-container-active:before {
content: none;
display: none;
}
.dashboard-tab-item {
position: relative;
z-index: 1;
display: inline-flex;
align-items: center;
justify-content: center;
overflow: visible;
margin: 5px 0 0 0;
padding: 0 8px 0 8px;
border: 1px solid var(--color-list-view-tabs-border);
border-radius: 12px;
background: transparent;
cursor: pointer;
font-family: inherit;
font-size: inherit;
line-height: inherit;
color: inherit;
box-shadow: none;
outline: none;
transition:
background 0.3s ease,
border-color 0.3s ease;
}
.dashboard-tab-item-icon {
display: flex;
align-items: center;
min-height: 30px;
line-height: 30px;
margin-right: 6px;
}
.dashboard-tab-item:focus-visible {
outline: 2px solid
color-mix(in srgb, var(--color-primary, #1677ff) 45%, transparent);
outline-offset: 2px;
}
.dashboard-tab-item:not(.dashboard-tab-item-active):hover {
background: var(--color-button-background);
border: 1px solid
color-mix(
in srgb,
var(--color-button-border) 40%,
var(--color-button-background)
);
}
.dashboard-tab-item-active {
z-index: 2;
align-self: stretch;
margin-bottom: -1px;
padding-bottom: 5px;
border-color: transparent;
border-radius: 12px 12px 0 0;
background: transparent;
}
.dashboard-tab-item-outline-container {
position: absolute;
top: -1px;
left: -1px;
right: -1px;
bottom: 0;
}
.dashboard-tab-item-outline {
position: absolute;
inset: 0 0 20px 0;
border: 1px solid var(--color-header-border);
border-bottom: none;
border-radius: 12px 12px 0 0;
pointer-events: none;
opacity: 0;
transition: opacity 0.3s ease;
}
.dashboard-tab-item-corner {
position: absolute;
bottom: -2px;
width: 22px;
height: 22px;
overflow: hidden;
pointer-events: none;
opacity: 0;
transition: opacity 0.3s ease;
}
.dashboard-tab-item-corner-left {
left: -21px;
}
.dashboard-tab-item-corner-right {
right: -21px;
}
.dashboard-tab-item-corner-inner {
position: absolute;
width: 48px;
height: 48px;
box-sizing: border-box;
border: 1px solid var(--color-header-border);
border-radius: 15.5px;
}
.dashboard-tab-item-corner-left .dashboard-tab-item-corner-inner {
bottom: 1px;
right: 1px;
}
.dashboard-tab-item-corner-right .dashboard-tab-item-corner-inner {
bottom: 1px;
left: 1px;
}
.dashboard-tab-item-active .dashboard-tab-item-outline,
.dashboard-tab-item-active .dashboard-tab-item-corner {
opacity: 1;
}
.dashboard-tabs-label {
max-width: 200px;
max-width: 280px;
margin-top: -2px;
}
@ -3395,7 +3580,36 @@ body.objectKanbanColumnResizing * {
.dashboard-tabs-add-button {
padding-inline: 4px;
min-width: 20px;
margin-bottom: 2px;
margin-top: 2px;
}
.dashboard-tab-item-dragging {
opacity: 0.4;
}
.dashboard-tab-item-drop-before::before,
.dashboard-tab-item-drop-after::after {
content: '';
position: absolute;
top: 4px;
bottom: 4px;
width: 2px;
border-radius: 1px;
background: var(--color-primary, #1677ff);
pointer-events: none;
}
.dashboard-tab-item-drop-before::before {
left: -5px;
}
.dashboard-tab-item-drop-after::after {
right: -5px;
}
.dashboard-tabs-reordering
.dashboard-tab-item:not(.dashboard-tab-item-dragging) {
cursor: default;
}
.dashboard-tab-panes {

View File

@ -19,6 +19,7 @@ import { useDashboardObjectTools } from '../../context/DashboardObjectToolsConte
import ScrollBox from '../../common/ScrollBox.jsx'
import TemplateEditor from '../../common/TemplateEditor.jsx'
import { useObjectNavigationTabPage } from '../../hooks/useObjectNavigationTabPage'
const log = loglevel.getLogger('DocumentTemplateDesign')
log.setLevel(config.logLevel)
@ -30,6 +31,11 @@ const DocumentTemplateDesign = () => {
const documentTemplateId = new URLSearchParams(location.search).get(
'documentTemplateId'
)
useObjectNavigationTabPage({
modelName: 'documentTemplate',
pageName: 'design',
objectId: documentTemplateId
})
const [collapseState, updateCollapseState] = useCollapseState(
'DocumentTemplateDesign',
{

View File

@ -16,6 +16,7 @@ import ObjectActions from '../../common/ObjectActions'
import TemplateEditor from '../../common/TemplateEditor'
import ScrollBox from '../../common/ScrollBox'
import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext'
import { useObjectNavigationTabPage } from '../../hooks/useObjectNavigationTabPage'
const EmailTemplateDesign = () => {
const location = useLocation()
@ -24,6 +25,11 @@ const EmailTemplateDesign = () => {
const emailTemplateId = new URLSearchParams(location.search).get(
'emailTemplateId'
)
useObjectNavigationTabPage({
modelName: 'emailTemplate',
pageName: 'design',
objectId: emailTemplateId
})
const [collapseState, updateCollapseState] = useCollapseState(
'EmailTemplateDesign',
{ preview: true, editor: true, notes: true }

View File

@ -21,7 +21,7 @@ import ActivityIndicator from '../../common/ActivityIndicator.jsx'
import ActionHandler from '../../common/ActionHandler.jsx'
import ObjectActions from '../../common/ObjectActions.jsx'
import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext'
import { useNavigationTabPage } from '../../context/NavigationTabsContext'
import { useObjectNavigationTabPage } from '../../hooks/useObjectNavigationTabPage'
import ObjectTable from '../../common/ObjectTable.jsx'
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
import InfoActionButtons from '../../common/InfoActionButtons.jsx'
@ -33,11 +33,15 @@ const log = loglevel.getLogger('NoteInfo')
log.setLevel(config.logLevel)
const NoteInfo = () => {
useNavigationTabPage({ title: 'Info - Note', modelName: 'note' })
const location = useLocation()
const objectFormRef = useRef(null)
const actionHandlerRef = useRef(null)
const noteId = new URLSearchParams(location.search).get('noteId')
useObjectNavigationTabPage({
modelName: 'note',
pageName: 'info',
objectId: noteId
})
const [collapseState, updateCollapseState] = useCollapseState('NoteInfo', {
info: true,
content: true,

View File

@ -21,6 +21,7 @@ import MissingPlaceholder from '../../common/MissingPlaceholder.jsx'
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
import UserNotifierToggle from '../../common/UserNotifierToggle.jsx'
import { useObjectNavigationTabPage } from '../../hooks/useObjectNavigationTabPage'
const log = loglevel.getLogger('GCodeFilePreview')
log.setLevel(config.logLevel)
@ -29,6 +30,11 @@ const GCodeFilePreview = () => {
const location = useLocation()
const objectFormRef = useRef(null)
const gcodeFileId = new URLSearchParams(location.search).get('gcodeFileId')
useObjectNavigationTabPage({
modelName: 'gcodeFile',
pageName: 'preview',
objectId: gcodeFileId
})
const [collapseState, updateCollapseState] = useCollapseState(
'GCodeFilePreview',
{

View File

@ -37,6 +37,7 @@ import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButt
import PrinterControlButtons from '../../common/PrinterControlButtons.jsx'
import PrinterQueue from '../../common/PrinterQueue.jsx'
import ListIcon from '../../../Icons/ListIcon.jsx'
import { useObjectNavigationTabPage } from '../../hooks/useObjectNavigationTabPage'
const log = loglevel.getLogger('ControlPrinter')
log.setLevel(config.logLevel)
@ -47,6 +48,11 @@ const ControlPrinter = ({ slicerIntegration = false }) => {
const actionHandlerRef = useRef(null)
const isMobile = useMediaQuery({ maxWidth: 768 })
const printerId = new URLSearchParams(location.search).get('printerId')
useObjectNavigationTabPage({
modelName: 'printer',
pageName: 'control',
objectId: printerId
})
const [collapseState, updateCollapseState] = useCollapseState(
'ControlPrinter',
{

View File

@ -280,75 +280,8 @@ const DashboardNavigation = () => {
/>
)
const navigationContents = (
<Flex style={{ width: '100%' }} align='center'>
{isMacOSApp ? <DashboardWindowButtons /> : null}
{isOtherApp ? (
<>
<WindowAppMenu />{' '}
<Divider
type='vertical'
style={{
height: '14px',
margin: showNavigationLabels
? '3px 3px 0 1.5px'
: '3px 0px 0 1.5px'
}}
/>
</>
) : null}
{showControls && isMobile && menu}
{showDesktopLogo == true ? (
<FarmControlLogo
style={{
fontSize: '200px',
height: '18px',
marginRight: '15px'
}}
/>
) : showMobileLogo == true ? (
<FarmControlLogoSmall
style={{
fontSize: '48px',
marginRight: isElectron ? '20px' : '25px'
}}
/>
) : null}
<div style={{ flexGrow: 1, minWidth: 0 }}>
{showControls && !isMobile ? (
<Flex align='center' style={{ width: '100%', minWidth: 0 }}>
{menu}
{isElectron ? (
<>
<Divider
type='vertical'
style={{
margin: showNavigationLabels
? '3px 16px 0 4px'
: '3px 16px 0 0',
height: '14px'
}}
/>
<DashboardTabs />
</>
) : null}
</Flex>
) : null}
{!showControls && (
<Text
type='secondary'
style={{
fontSize: '14px',
marginLeft: '8px',
userSelect: 'none',
'--webkit-user-select': 'none'
}}
>
Farm Control
</Text>
)}
</div>
const navigationTrailing = (
<>
<div className='electrobun-webkit-app-region-no-drag'>
<Flex
gap={'small'}
@ -461,17 +394,117 @@ const DashboardNavigation = () => {
</Flex>
</div>
{isOtherApp ? <DashboardWindowButtons /> : null}
</>
)
const navigationLeading = (
<>
{isMacOSApp ? <DashboardWindowButtons /> : null}
{isOtherApp ? (
<>
<WindowAppMenu />{' '}
<Divider
type='vertical'
style={{
height: '14px',
margin: showNavigationLabels
? '3px 3px 0 1.5px'
: '3px 0px 0 1.5px'
}}
/>
</>
) : null}
{showControls && isMobile && menu}
{showDesktopLogo == true ? (
<FarmControlLogo
style={{
fontSize: '200px',
height: '18px',
marginRight: '15px'
}}
/>
) : showMobileLogo == true ? (
<FarmControlLogoSmall
style={{
fontSize: '48px',
marginRight: isElectron ? '20px' : '25px'
}}
/>
) : null}
</>
)
const navigationContents = (
<Flex style={{ width: '100%' }} align='center'>
{navigationLeading}
<div style={{ flexGrow: 1, minWidth: 0 }}>
{showControls && !isMobile ? (
<Flex align='center' style={{ width: '100%', minWidth: 0 }}>
{menu}
</Flex>
) : null}
{!showControls && (
<Text
type='secondary'
style={{
fontSize: '14px',
marginLeft: '8px',
userSelect: 'none',
'--webkit-user-select': 'none'
}}
>
Farm Control
</Text>
)}
</div>
{navigationTrailing}
</Flex>
)
const electronDragClass =
isFullScreen || (isMaximized && isOtherApp)
? 'electrobun-webkit-app-region-no-drag'
: 'electrobun-webkit-app-region-drag'
return (
<>
{isElectron ? (
<Flex
className={`ant-menu-horizontal electron-navigation-wrapper ant-menu-light ${isFullScreen || (isMaximized && isOtherApp) ? 'electrobun-webkit-app-region-no-drag' : 'electrobun-webkit-app-region-drag'}`}
className={`electron-navigation-wrapper ${electronDragClass}`}
style={{ lineHeight: '40px', padding: '0 2px 0 2px' }}
>
{navigationContents}
<Flex align='center' className='electron-navigation-leading'>
{navigationLeading}
{showControls && !isMobile ? menu : null}
{showControls && !isMobile ? (
<Divider
type='vertical'
style={{
margin: showNavigationLabels
? '3px 16px 0 4px'
: '3px 16px 0 0',
height: '14px'
}}
/>
) : null}
{!showControls ? (
<Text
type='secondary'
style={{
fontSize: '14px',
marginLeft: '8px',
userSelect: 'none',
'--webkit-user-select': 'none'
}}
>
Farm Control
</Text>
) : null}
</Flex>
{showControls && !isMobile ? <DashboardTabs /> : null}
<Flex align='center' className='electron-navigation-trailing'>
{navigationTrailing}
</Flex>
</Flex>
) : (
<Flex vertical>

View File

@ -1,8 +1,7 @@
import PropTypes from 'prop-types'
import { useMemo } from 'react'
import { useCallback, useState } from 'react'
import { Button, Flex, Typography } from 'antd'
import classNames from 'classnames'
import SegmentedNav from './SegmentedNav'
import ScrollBox from './ScrollBox'
import PlusIcon from '../../Icons/PlusIcon'
import XMarkIcon from '../../Icons/XMarkIcon'
@ -14,9 +13,29 @@ 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: 160 }}>
<Text ellipsis style={{ maxWidth: 240 }}>
{tab.title || 'Farm Control'}
</Text>
<span
@ -55,30 +74,31 @@ const DashboardTabs = () => {
handleTabDragEnd,
handleExternalTabDrop
} = useNavigationTabs()
const [draggedValue, setDraggedValue] = useState(null)
const [dropTarget, setDropTarget] = useState(null)
const options = useMemo(
() =>
tabs.map((tab) => {
const model = tab.modelName ? getModelByName(tab.modelName) : null
const Icon = model?.icon || HomeIcon
return {
value: tab.id,
reorderable: true,
icon: (
<Icon
style={{ fontSize: 14, marginTop: '-2px' }}
color='secondary'
/>
),
label: <DashboardTabLabel tab={tab} onClose={closeTab} />
}
}),
[closeTab, tabs]
)
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()
@ -86,6 +106,105 @@ const DashboardTabs = () => {
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()
@ -99,34 +218,105 @@ const DashboardTabs = () => {
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='center'
align='flex-start'
gap={4}
className={classNames(
'dashboard-tabs',
'electrobun-webkit-app-region-no-drag'
'electrobun-webkit-app-region-drag'
)}
style={{ flex: 1, minWidth: 0 }}
onDragOver={handleStripDragOver}
onDrop={handleStripDrop}
>
<ScrollBox className='dashboard-tabs-segmented-wrap' horizontal>
<Flex align='center' gap={8} className='dashboard-tabs-segmented-inner'>
<SegmentedNav
className='dashboard-tabs-segmented'
value={activeTabId}
options={options}
onChange={selectTab}
onReorder={reorderTabs}
onExternalDrop={handleExternalTabDrop}
onItemDragStart={handleItemDragStart}
onItemDragEnd={handleTabDragEnd}
/>
<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'
className='dashboard-tabs-add-button electrobun-webkit-app-region-no-drag'
onClick={addTab}
icon={<PlusIcon style={{ fontSize: 14, marginTop: 3 }} />}
/>

View File

@ -7,7 +7,8 @@ import { getModelByName } from '../../../database/ObjectModels'
import { getObjectIdFromSearch } from '../../../utils/modelActions'
import { useActions } from '../context/ActionsContext'
import { AuthContext } from '../context/AuthContext'
import { useNavigationTabPage, useIsNavigationTabActive } from '../context/NavigationTabsContext'
import { useIsNavigationTabActive } from '../context/NavigationTabsContext'
import { useObjectNavigationTabPage } from '../hooks/useObjectNavigationTabPage'
const ModelPage = ({ modelName, pageName }) => {
const model = getModelByName(modelName)
@ -16,9 +17,8 @@ const ModelPage = ({ modelName, pageName }) => {
const { setCurrentObject, setCurrentObjectType } = useActions()
const { userProfile } = useContext(AuthContext)
const objectId = getObjectIdFromSearch(modelName, location.search)
const pageTitle = `${pageName.charAt(0).toUpperCase()}${pageName.slice(1)} - ${model?.label || modelName}`
const isTabActive = useIsNavigationTabActive()
useNavigationTabPage({ title: pageTitle, modelName })
useObjectNavigationTabPage({ modelName, pageName, objectId })
useEffect(() => {
if (!isTabActive) return undefined

View File

@ -265,7 +265,10 @@ const SegmentedNav = ({
const handleGroupDragOver = useCallback(
(event) => {
const isExternal = acceptsExternal && hasExternalTabDrag(event)
if ((!onReorder && !isExternal) || (draggedValue == null && !isExternal)) {
if (
(!onReorder && !isExternal) ||
(draggedValue == null && !isExternal)
) {
return
}
@ -296,7 +299,10 @@ const SegmentedNav = ({
const handleGroupDrop = useCallback(
(event) => {
const isExternal = acceptsExternal && draggedValue == null
if ((!onReorder && !isExternal) || (draggedValue == null && !isExternal)) {
if (
(!onReorder && !isExternal) ||
(draggedValue == null && !isExternal)
) {
return
}

View File

@ -131,7 +131,9 @@ export const ObjectListViewProvider = ({ children, objectType, tableRef }) => {
const { getViewFromUrl, persistView, setObjectListView } = useTableState()
const listModel = getModelByName(objectType)
useNavigationTabPage({
title: listModel?.label ? `List - ${listModel.label}` : 'List',
title: listModel?.labelPlural
? `List - ${listModel.labelPlural}`
: 'List',
modelName: objectType
})

View File

@ -216,6 +216,10 @@ export const ThemeProvider = ({ children }) => {
'--color-mobile-footer-item-border',
isDarkMode ? '#303030' : '#f0f0f0'
)
root.style.setProperty(
'--color-header-border',
isDarkMode ? 'rgba(253,253,253,0.12)' : 'rgba(10,10,10,0.12)'
)
}, [isDarkMode, primaryColorOverride])
const themeConfig = {

View File

@ -0,0 +1,82 @@
import { useContext, useEffect, useRef, useState } from 'react'
import { getModelByName } from '../../../database/ObjectModels'
import { ApiServerContext } from '../context/ApiServerContext'
import { AuthContext } from '../context/AuthContext'
import { useNavigationTabPage } from '../context/NavigationTabsContext'
const formatPageLabel = (pageName) =>
pageName
? `${pageName.charAt(0).toUpperCase()}${pageName.slice(1)}`
: 'Page'
const getObjectDisplayName = (objectData, model) => {
if (objectData?.name) return objectData.name
if (objectData?._reference && model?.prefix) {
return `${model.prefix}:${objectData._reference}`
}
return null
}
export const formatObjectPageTabTitle = ({
pageName,
model,
objectName
} = {}) => {
const pageLabel = formatPageLabel(pageName)
const collectionLabel = model?.labelPlural
if (objectName && collectionLabel) {
return `${pageLabel} (${objectName}) - ${collectionLabel}`
}
if (objectName) return `${pageLabel} (${objectName})`
if (collectionLabel) return `${pageLabel} - ${collectionLabel}`
return pageLabel
}
export const useObjectNavigationTabPage = ({
modelName,
pageName,
objectId
} = {}) => {
const model = getModelByName(modelName)
const { fetchSpotlightData, subscribeToObjectUpdates, connected } =
useContext(ApiServerContext)
const { token } = useContext(AuthContext)
const [objectName, setObjectName] = useState(null)
const fetchSpotlightDataRef = useRef(fetchSpotlightData)
fetchSpotlightDataRef.current = fetchSpotlightData
useEffect(() => {
if (!objectId || !model?.prefix || token == null) {
setObjectName(null)
return undefined
}
let cancelled = false
const query = `${model.prefix}:${objectId}`
void fetchSpotlightDataRef.current(query).then((result) => {
if (cancelled) return
if (result && typeof result === 'object' && !Array.isArray(result)) {
setObjectName(getObjectDisplayName(result, model))
}
})
return () => {
cancelled = true
}
}, [model, objectId, token])
useEffect(() => {
if (!objectId || !modelName || !connected) return undefined
return subscribeToObjectUpdates(objectId, modelName, (value) => {
setObjectName((current) => getObjectDisplayName(value, model) || current)
})
}, [connected, model, modelName, objectId, subscribeToObjectUpdates])
useNavigationTabPage({
title: formatObjectPageTabTitle({ pageName, model, objectName }),
modelName
})
}
export default useObjectNavigationTabPage