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 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,34 +683,14 @@ 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)
|
||||||
const activeTabIndex = activeTab
|
const activeTabIndex = activeTab
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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'}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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) => {
|
||||||
|
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'
|
||||||
|
)
|
||||||
|
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(
|
const currentTab = tabsRef.current.find(
|
||||||
(tab) => tab.id === activeTabIdRef.current
|
(tab) => tab.id === activeTabIdRef.current
|
||||||
)
|
)
|
||||||
const nextTab = cloneCurrentPageTab(currentTab, location)
|
const entry = targetUrl
|
||||||
|
? pathToEntry(targetUrl)
|
||||||
|
: locationToEntry(location)
|
||||||
|
const nextTab = targetUrl
|
||||||
|
? createTabFromLocation(entry)
|
||||||
|
: cloneCurrentPageTab(currentTab, location)
|
||||||
|
|
||||||
setTabs((current) => [...current, nextTab])
|
setTabs((current) => [...current, nextTab])
|
||||||
setActiveTabId(nextTab.id)
|
setActiveTabId(nextTab.id)
|
||||||
}, [location])
|
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,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user