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 listRef = useRef(null)
const cloneRef = useRef(null) const cloneRef = useRef(null)
const lineRef = 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('|')}` const tabLayoutKey = `${activeTabId}:${tabs.map((tab) => `${tab.id}:${tab.title || ''}`).join('|')}`
useLayoutEffect(() => { useLayoutEffect(() => {
@ -509,21 +507,6 @@ const useActiveTabStickyScroll = (activeTabId, tabs) => {
} }
}, [tabLayoutKey]) }, [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 } return { rootRef, listRef, cloneRef, lineRef }
} }
@ -533,6 +516,7 @@ const DashboardTabs = () => {
activeTabId, activeTabId,
selectTab, selectTab,
addTab, addTab,
registerTabStripRoot,
closeTab, closeTab,
reorderTabs, reorderTabs,
handleTabDragStart, handleTabDragStart,
@ -699,33 +683,13 @@ const DashboardTabs = () => {
selectTab(tabs[nextIndex].id) selectTab(tabs[nextIndex].id)
} }
const handleAddTab = () => { const setRootRef = useCallback(
addTab() (element) => {
rootRef.current = element
const scrollEl = rootRef.current?.querySelector( registerTabStripRoot(element)
'.simplebar-content-wrapper' },
) [registerTabStripRoot]
)
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 isReordering = draggedValue != null || dropTarget != null const isReordering = draggedValue != null || dropTarget != null
const activeTab = tabs.find((tab) => tab.id === activeTabId) const activeTab = tabs.find((tab) => tab.id === activeTabId)
@ -775,7 +739,7 @@ const DashboardTabs = () => {
return ( return (
<Flex <Flex
ref={rootRef} ref={setRootRef}
align='flex-start' align='flex-start'
gap={4} gap={4}
className={classNames( className={classNames(
@ -836,7 +800,7 @@ const DashboardTabs = () => {
<Button <Button
type='text' type='text'
className='dashboard-tabs-add-button electrobun-webkit-app-region-no-drag' className='dashboard-tabs-add-button electrobun-webkit-app-region-no-drag'
onClick={handleAddTab} onClick={() => addTab()}
icon={<PlusIcon style={{ fontSize: 14, marginTop: 3 }} />} icon={<PlusIcon style={{ fontSize: 14, marginTop: 3 }} />}
/> />
</div> </div>

View File

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

View File

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

View File

@ -60,6 +60,21 @@ const locationToEntry = (location) => ({
hash: location?.hash || '' 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) => { const entryToPath = (entry) => {
if (!entry) return '/' if (!entry) return '/'
if (typeof entry === 'string') return entry if (typeof entry === 'string') return entry
@ -188,6 +203,8 @@ export const NavigationTabsProvider = ({ children }) => {
const captureQueueRef = useRef(Promise.resolve()) const captureQueueRef = useRef(Promise.resolve())
const captureInFlightRef = useRef(new Map()) const captureInFlightRef = useRef(new Map())
const previewThemeRef = useRef(isDarkMode ? 'dark' : 'light') const previewThemeRef = useRef(isDarkMode ? 'dark' : 'light')
const tabStripRootRef = useRef(null)
const scrollTabStripToEndIntervalRef = useRef(null)
tabsRef.current = tabs tabsRef.current = tabs
activeTabIdRef.current = activeTabId activeTabIdRef.current = activeTabId
locationRef.current = location locationRef.current = location
@ -251,14 +268,75 @@ export const NavigationTabsProvider = ({ children }) => {
[restoreToEntry] [restoreToEntry]
) )
const addTab = useCallback(() => { const registerTabStripRoot = useCallback((element) => {
const currentTab = tabsRef.current.find( tabStripRootRef.current = element
(tab) => tab.id === activeTabIdRef.current }, [])
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) if (!scrollEl) return
setTabs((current) => [...current, nextTab])
setActiveTabId(nextTab.id) let lastMaxScrollLeft = scrollEl.scrollWidth - scrollEl.clientWidth
}, [location]) 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( const removeTab = useCallback(
(tabId, { closeWindowIfLast = true } = {}) => { (tabId, { closeWindowIfLast = true } = {}) => {
@ -689,6 +767,7 @@ export const NavigationTabsProvider = ({ children }) => {
canGoForward, canGoForward,
createNewWindow, createNewWindow,
registerTabPane, registerTabPane,
registerTabStripRoot,
captureTabPreview captureTabPreview
}), }),
[ [
@ -709,6 +788,7 @@ export const NavigationTabsProvider = ({ children }) => {
hydrated, hydrated,
isElectron, isElectron,
registerTabPane, registerTabPane,
registerTabStripRoot,
reorderTabs, reorderTabs,
selectTab, selectTab,
setTabPage, setTabPage,