Refactor DashboardTabs and ObjectDisplay for Enhanced Navigation and Scrolling

- Removed unused state references and optimized scrolling logic in DashboardTabs for improved performance.
- Updated ObjectDisplay to utilize action context for navigation, enhancing user interaction with object details.
- Introduced new utility functions for handling tab entries and navigation, streamlining tab management and improving user experience.
This commit is contained in:
Tom Butcher 2026-09-20 23:36:22 +01:00
parent 4c1766044f
commit 44c8729787
4 changed files with 155 additions and 72 deletions

View File

@ -469,8 +469,6 @@ const useActiveTabStickyScroll = (activeTabId, tabs) => {
const listRef = useRef(null)
const cloneRef = useRef(null)
const lineRef = useRef(null)
const prevTabCountRef = useRef(tabs.length)
const pendingScrollTabIdRef = useRef(null)
const tabLayoutKey = `${activeTabId}:${tabs.map((tab) => `${tab.id}:${tab.title || ''}`).join('|')}`
useLayoutEffect(() => {
@ -509,21 +507,6 @@ const useActiveTabStickyScroll = (activeTabId, tabs) => {
}
}, [tabLayoutKey])
useEffect(() => {
const lastTab = tabs[tabs.length - 1]
if (
tabs.length === prevTabCountRef.current + 1 &&
lastTab?.id === activeTabId
) {
pendingScrollTabIdRef.current = lastTab.id
}
prevTabCountRef.current = tabs.length
if (!lastTab || pendingScrollTabIdRef.current !== lastTab.id) {
return undefined
}
}, [activeTabId, tabs])
return { rootRef, listRef, cloneRef, lineRef }
}
@ -533,6 +516,7 @@ const DashboardTabs = () => {
activeTabId,
selectTab,
addTab,
registerTabStripRoot,
closeTab,
reorderTabs,
handleTabDragStart,
@ -699,33 +683,13 @@ const DashboardTabs = () => {
selectTab(tabs[nextIndex].id)
}
const handleAddTab = () => {
addTab()
const scrollEl = rootRef.current?.querySelector(
'.simplebar-content-wrapper'
)
if (!scrollEl) return
let lastMaxScrollLeft = scrollEl.scrollWidth - scrollEl.clientWidth
let unchangedChecks = 0
const interval = setInterval(() => {
const maxScrollLeft = scrollEl.scrollWidth - scrollEl.clientWidth
if (maxScrollLeft !== lastMaxScrollLeft) {
scrollEl.scrollLeft = maxScrollLeft
lastMaxScrollLeft = maxScrollLeft
unchangedChecks = 0
} else {
unchangedChecks++
}
if (unchangedChecks >= 15) {
clearInterval(interval)
}
}, 10)
}
const setRootRef = useCallback(
(element) => {
rootRef.current = element
registerTabStripRoot(element)
},
[registerTabStripRoot]
)
const isReordering = draggedValue != null || dropTarget != null
const activeTab = tabs.find((tab) => tab.id === activeTabId)
@ -775,7 +739,7 @@ const DashboardTabs = () => {
return (
<Flex
ref={rootRef}
ref={setRootRef}
align='flex-start'
gap={4}
className={classNames(
@ -836,7 +800,7 @@ const DashboardTabs = () => {
<Button
type='text'
className='dashboard-tabs-add-button electrobun-webkit-app-region-no-drag'
onClick={handleAddTab}
onClick={() => addTab()}
icon={<PlusIcon style={{ fontSize: 14, marginTop: 3 }} />}
/>
</div>

View File

@ -2,11 +2,11 @@ import PropTypes from 'prop-types'
import { Typography, Flex, Badge, Tag, Popover } from 'antd'
import { LoadingOutlined } from '@ant-design/icons'
import { useState, useEffect, useContext, useCallback, useRef } from 'react'
import { useNavigate } from 'react-router-dom'
import { getModelByName } from '../../../database/ObjectModels'
import { buildActionUrl } from '../../../utils/modelActions'
import { findAction } from '../../../utils/modelActions'
import { ApiServerContext } from '../context/ApiServerContext'
import { AuthContext } from '../context/AuthContext'
import { useActions } from '../context/ActionsContext'
import merge from 'lodash/merge'
import IdDisplay from './IdDisplay'
import ElipsisText from './ElipsisText'
@ -26,7 +26,7 @@ const ObjectDisplay = ({
const { subscribeToObjectUpdates, connected, fetchSpotlightData } =
useContext(ApiServerContext)
const { token } = useContext(AuthContext)
const navigate = useNavigate()
const { callAction } = useActions()
const idRef = useRef(null)
// Update event handler
@ -175,17 +175,9 @@ const ObjectDisplay = ({
const model = getModelByName(objectType)
const Icon = model.icon
const prefix = model.prefix
// Get hyperlink URL from model's default actions
var hyperlink = null
const defaultModelActions =
model.actions?.filter((action) => action.default == true) || []
const infoAction = findAction(model, 'info')
const objectId = getStringId(objectData)
if (defaultModelActions.length >= 1 && objectId) {
hyperlink = buildActionUrl(model, defaultModelActions[0], objectId, '', '')
}
// Render name with spotlight support
const renderNameDisplay = () => {
if (!objectData?.name) return null
@ -215,7 +207,12 @@ const ObjectDisplay = ({
return textElement
}
const canNavigate = showHyperlink && hyperlink != null
const canNavigate = showHyperlink && infoAction != null && objectId
const handleNavigate = () => {
if (!canNavigate) return
callAction(infoAction, { ...objectData, _id: objectId }, objectType)
}
return (
<Tag
@ -226,7 +223,7 @@ const ObjectDisplay = ({
cursor: canNavigate ? 'pointer' : undefined
}}
className='object-display-tag'
onClick={canNavigate ? () => navigate(hyperlink) : undefined}
onClick={canNavigate ? handleNavigate : undefined}
>
<Flex
gap={objectData?.color || objectData?.state ? 'small' : '5px'}

View File

@ -21,6 +21,8 @@ import {
stripModalActionParams
} from '../../../utils/modelActions'
import { AuthContext } from './AuthContext'
import { ElectronContext } from './ElectronContext'
import { useNavigationTabs } from './NavigationTabsContext'
const ActionsContext = createContext()
@ -35,6 +37,8 @@ const ActionsProvider = ({ children }) => {
const [modalAction, setModalAction] = useState(null)
const [onModalOk, setOnModalOk] = useState(null)
const lastHandledAction = useRef(null)
const { isElectron } = useContext(ElectronContext)
const { addTab } = useNavigationTabs()
const searchParams = new URLSearchParams(location.search)
const actionName = searchParams.get('action')
@ -55,6 +59,44 @@ const ActionsProvider = ({ children }) => {
clearAction()
}, [clearAction, onModalOk])
const [ctrlDown, setCtrlDown] = useState(false)
useEffect(() => {
const handleKeyDown = (event) => {
const key = event.key
if (key === 'Meta') {
setCtrlDown(true)
}
}
const handleKeyUp = (event) => {
const key = event.key
if (key === 'Meta') {
setCtrlDown(false)
}
}
window.addEventListener('keydown', handleKeyDown)
window.addEventListener('keyup', handleKeyUp)
return () => {
window.removeEventListener('keydown', handleKeyDown)
window.removeEventListener('keyup', handleKeyUp)
}
}, [])
const handleNavigate = useCallback(
(url) => {
if (isElectron && ctrlDown) {
addTab(url)
return
}
if (!isElectron && ctrlDown) {
window.open(url, '_blank')
return
}
navigate(url, { replace: true })
},
[addTab, navigate, ctrlDown, isElectron]
)
const callAction = useCallback(
(action, objectData, objectType) => {
setCurrentObject(objectData)
@ -68,9 +110,9 @@ const ActionsProvider = ({ children }) => {
loc.pathname,
loc.search
)
navigate(url)
handleNavigate(url)
},
[navigate]
[handleNavigate]
)
useEffect(() => {
@ -128,7 +170,7 @@ const ActionsProvider = ({ children }) => {
if (action.name === 'list') {
lastHandledAction.current = actionKey
if (location.pathname !== model.url) {
navigate(model.url, { replace: true })
handleNavigate(model.url)
}
return
}
@ -152,7 +194,7 @@ const ActionsProvider = ({ children }) => {
location.pathname,
location.search
)
navigate(url, { replace: true })
handleNavigate(url)
}
}, [
actionName,
@ -160,7 +202,7 @@ const ActionsProvider = ({ children }) => {
currentObject,
location.pathname,
location.search,
navigate
handleNavigate
])
const modalObjectData = currentObject

View File

@ -60,6 +60,21 @@ const locationToEntry = (location) => ({
hash: location?.hash || ''
})
const pathToEntry = (url) => {
if (!url) return locationToEntry({ pathname: '/' })
if (typeof url !== 'string') return locationToEntry(url)
try {
const parsed = new URL(url, window.location.origin)
return {
pathname: parsed.pathname || '/',
search: parsed.search || '',
hash: parsed.hash || ''
}
} catch {
return locationToEntry({ pathname: url })
}
}
const entryToPath = (entry) => {
if (!entry) return '/'
if (typeof entry === 'string') return entry
@ -188,6 +203,8 @@ export const NavigationTabsProvider = ({ children }) => {
const captureQueueRef = useRef(Promise.resolve())
const captureInFlightRef = useRef(new Map())
const previewThemeRef = useRef(isDarkMode ? 'dark' : 'light')
const tabStripRootRef = useRef(null)
const scrollTabStripToEndIntervalRef = useRef(null)
tabsRef.current = tabs
activeTabIdRef.current = activeTabId
locationRef.current = location
@ -251,14 +268,75 @@ export const NavigationTabsProvider = ({ children }) => {
[restoreToEntry]
)
const addTab = useCallback(() => {
const currentTab = tabsRef.current.find(
(tab) => tab.id === activeTabIdRef.current
const registerTabStripRoot = useCallback((element) => {
tabStripRootRef.current = element
}, [])
useEffect(() => {
return () => {
if (scrollTabStripToEndIntervalRef.current) {
clearInterval(scrollTabStripToEndIntervalRef.current)
scrollTabStripToEndIntervalRef.current = null
}
}
}, [])
const scrollTabStripToEnd = useCallback(() => {
if (scrollTabStripToEndIntervalRef.current) {
clearInterval(scrollTabStripToEndIntervalRef.current)
scrollTabStripToEndIntervalRef.current = null
}
const scrollEl = tabStripRootRef.current?.querySelector(
'.simplebar-content-wrapper'
)
const nextTab = cloneCurrentPageTab(currentTab, location)
setTabs((current) => [...current, nextTab])
setActiveTabId(nextTab.id)
}, [location])
if (!scrollEl) return
let lastMaxScrollLeft = scrollEl.scrollWidth - scrollEl.clientWidth
let unchangedChecks = 0
scrollTabStripToEndIntervalRef.current = setInterval(() => {
const maxScrollLeft = scrollEl.scrollWidth - scrollEl.clientWidth
if (maxScrollLeft !== lastMaxScrollLeft) {
scrollEl.scrollLeft = maxScrollLeft
lastMaxScrollLeft = maxScrollLeft
unchangedChecks = 0
} else {
unchangedChecks++
}
if (unchangedChecks >= 15) {
clearInterval(scrollTabStripToEndIntervalRef.current)
scrollTabStripToEndIntervalRef.current = null
}
}, 10)
}, [])
const addTab = useCallback(
(url) => {
const targetUrl = typeof url === 'string' ? url : null
const currentTab = tabsRef.current.find(
(tab) => tab.id === activeTabIdRef.current
)
const entry = targetUrl
? pathToEntry(targetUrl)
: locationToEntry(location)
const nextTab = targetUrl
? createTabFromLocation(entry)
: cloneCurrentPageTab(currentTab, location)
setTabs((current) => [...current, nextTab])
setActiveTabId(nextTab.id)
activeTabIdRef.current = nextTab.id
if (targetUrl && !entriesEqual(entry, locationRef.current)) {
restoreToEntry(entry)
}
scrollTabStripToEnd()
},
[location, restoreToEntry, scrollTabStripToEnd]
)
const removeTab = useCallback(
(tabId, { closeWindowIfLast = true } = {}) => {
@ -689,6 +767,7 @@ export const NavigationTabsProvider = ({ children }) => {
canGoForward,
createNewWindow,
registerTabPane,
registerTabStripRoot,
captureTabPreview
}),
[
@ -709,6 +788,7 @@ export const NavigationTabsProvider = ({ children }) => {
hydrated,
isElectron,
registerTabPane,
registerTabStripRoot,
reorderTabs,
selectTab,
setTabPage,