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:
parent
4c1766044f
commit
44c8729787
@ -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>
|
||||
|
||||
@ -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'}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user