Add Dashboard Object Tools Context and Chevron Icons

- Introduced DashboardObjectToolsContext and DashboardObjectToolsProvider for managing object tools state within the dashboard.
- Added ChevronLeftIcon and ChevronRightIcon components for navigation buttons, enhancing user interface consistency.
- Updated ObjectTableNavigationButtons to utilize new chevron icons and improved layout for better user experience.
- Refactored KeyboardShortcut and Tooltip components for cleaner code and improved functionality.
This commit is contained in:
Tom Butcher 2026-08-21 21:11:15 +01:00
parent 650d9a8513
commit 42d6ac6422
8 changed files with 146 additions and 43 deletions

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(-1.001515,0,0,1.001515,62.999788,3.331628)">
<path d="M32.953,57.25C34.312,57.25 35.438,56.766 36.406,55.828L60.047,32.719C61.312,31.438 61.906,30.172 61.906,28.625C61.891,27.047 61.297,25.766 60.047,24.531L36.406,1.422C35.438,0.469 34.328,0 32.953,0C30.172,0 27.906,2.234 27.906,4.984C27.906,6.328 28.484,7.609 29.531,8.641L51.438,29.859L51.438,27.312L29.531,48.609C28.5,49.625 27.906,50.906 27.906,52.281C27.906,55.016 30.172,57.25 32.953,57.25Z" style="fill-rule:nonzero;"/>
<path d="M5.016,57.25C6.375,57.25 7.5,56.766 8.484,55.828L32.125,32.719C33.406,31.438 33.984,30.172 33.984,28.625C33.953,27.047 33.391,25.766 32.125,24.531L8.469,1.422C7.531,0.469 6.391,0 5.016,0C2.234,0 0,2.234 0,4.984C0,6.328 0.562,7.609 1.594,8.641L23.516,29.859L23.516,27.312L1.594,48.609C0.578,49.625 0,50.906 0,52.281C0,55.016 2.234,57.25 5.016,57.25Z" style="fill-rule:nonzero;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -19,6 +19,7 @@ import {
} from './components/Dashboard/context/SpotlightContext.jsx' } from './components/Dashboard/context/SpotlightContext.jsx'
import { ActionsModalProvider } from './components/Dashboard/context/ActionsModalContext.jsx' import { ActionsModalProvider } from './components/Dashboard/context/ActionsModalContext.jsx'
import { ActionsProvider } from './components/Dashboard/context/ActionsContext.jsx' import { ActionsProvider } from './components/Dashboard/context/ActionsContext.jsx'
import { DashboardObjectToolsProvider } from './components/Dashboard/context/DashboardObjectToolsContext.jsx'
import MissingPlaceholder from './components/Dashboard/common/MissingPlaceholder.jsx' import MissingPlaceholder from './components/Dashboard/common/MissingPlaceholder.jsx'
import { import {
@ -93,6 +94,7 @@ const AppContent = () => {
<SpotlightProvider> <SpotlightProvider>
<ActionsModalProvider> <ActionsModalProvider>
<ActionsProvider> <ActionsProvider>
<DashboardObjectToolsProvider>
<Routes> <Routes>
<Route <Route
path='/applaunch' path='/applaunch'
@ -164,6 +166,7 @@ const AppContent = () => {
} }
/> />
</Routes> </Routes>
</DashboardObjectToolsProvider>
</ActionsProvider> </ActionsProvider>
</ActionsModalProvider> </ActionsModalProvider>
</SpotlightProvider> </SpotlightProvider>

View File

@ -13,6 +13,7 @@ import DeveloperSidebar from './Developer/DeveloperSidebar'
import DashboardSidebarSplitter from './common/DashboardSidebarSplitter' import DashboardSidebarSplitter from './common/DashboardSidebarSplitter'
import { useThemeContext } from './context/ThemeContext' import { useThemeContext } from './context/ThemeContext'
import { MessageProvider } from './context/MessageContext' import { MessageProvider } from './context/MessageContext'
import { useDashboardObjectToolsContext } from './context/DashboardObjectToolsContext'
const { Content } = Layout const { Content } = Layout
@ -26,6 +27,7 @@ const DashboardLayout = ({ children }) => {
const isDeveloper = location.pathname.startsWith('/dashboard/developer') const isDeveloper = location.pathname.startsWith('/dashboard/developer')
const { isDarkMode } = useThemeContext() const { isDarkMode } = useThemeContext()
const { currentObjectTools } = useDashboardObjectToolsContext()
const sidebar = isProduction ? ( const sidebar = isProduction ? (
<ProductionSidebar /> <ProductionSidebar />
@ -57,8 +59,9 @@ const DashboardLayout = ({ children }) => {
> >
<Content> <Content>
<Flex vertical style={{ height: '100%' }} gap='20px'> <Flex vertical style={{ height: '100%' }} gap='20px'>
<Flex justify='space-between'> <Flex justify='space-between' align='center'>
<DashboardBreadcrumb style={{ margin: '16px 0' }} /> <DashboardBreadcrumb style={{ margin: '16px 0' }} />
{currentObjectTools}
</Flex> </Flex>
{children} {children}
</Flex> </Flex>

View File

@ -1,4 +1,4 @@
import { useEffect, useRef, cloneElement, useMemo } from 'react' import { useEffect, useRef, useMemo } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import loglevel from 'loglevel' import loglevel from 'loglevel'
import config from '../../../config' import config from '../../../config'
@ -50,7 +50,6 @@ const KeyboardShortcut = ({
onTrigger, onTrigger,
log = false log = false
}) => { }) => {
const childRef = useRef()
const pressedKeysRef = useRef(new Set()) const pressedKeysRef = useRef(new Set())
const shortcutKeys = useMemo(() => parseShortcut(shortcut), [shortcut]) const shortcutKeys = useMemo(() => parseShortcut(shortcut), [shortcut])
@ -94,8 +93,6 @@ const KeyboardShortcut = ({
} }
}, [shortcutKeys, onTrigger, log]) }, [shortcutKeys, onTrigger, log])
const element = cloneElement(children, { ref: childRef })
if (hint) { if (hint) {
var osSpecificHint = hint.replaceAll('ALT', '⌥') var osSpecificHint = hint.replaceAll('ALT', '⌥')
@ -104,11 +101,11 @@ const KeyboardShortcut = ({
hideBorder hideBorder
title={<KeyboardKey size='md'>{osSpecificHint}</KeyboardKey>} title={<KeyboardKey size='md'>{osSpecificHint}</KeyboardKey>}
> >
{element} {children}
</Tooltip> </Tooltip>
) )
} }
return element return children
} }
KeyboardShortcut.propTypes = { KeyboardShortcut.propTypes = {

View File

@ -1,8 +1,7 @@
import { useContext, useEffect, useState, useCallback, useRef } from 'react' import { useContext, useEffect, useState, useCallback, useRef } from 'react'
import { Button, Divider, Flex, Space } from 'antd' import { Button, Divider, Flex, Space } from 'antd'
import { useSearchParams } from 'react-router-dom' import { useSearchParams } from 'react-router-dom'
import ArrowLeftIcon from '../../Icons/ArrowLeftIcon.jsx'
import ArrowRightIcon from '../../Icons/ArrowRightIcon.jsx'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { ApiServerContext } from '../context/ApiServerContext' import { ApiServerContext } from '../context/ApiServerContext'
import { AuthContext } from '../context/AuthContext' import { AuthContext } from '../context/AuthContext'
@ -11,6 +10,8 @@ import {
useTableStatePersistence useTableStatePersistence
} from '../context/TableStateContext' } from '../context/TableStateContext'
import KeyboardShortcut from './KeyboardShortcut.jsx' import KeyboardShortcut from './KeyboardShortcut.jsx'
import ChevronLeftIcon from '../../Icons/ChevronLeftIcon.jsx'
import ChevronRightIcon from '../../Icons/ChevronRightIcon.jsx'
const ObjectTableNavigationButtons = ({ const ObjectTableNavigationButtons = ({
disabled, disabled,
@ -25,10 +26,10 @@ const ObjectTableNavigationButtons = ({
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const { getPersistedFilter, getPersistedSorter } = useTableStatePersistence({ const { getPersistedFilter, getPersistedSorter } = useTableStatePersistence({
scope: objectType, scope: objectType,
useFilterInSession: true, useFilterInSession: true,
useSortInSession: true useSortInSession: true
}) })
const getObjectNeighborsRef = useRef(getObjectNeighbors) const getObjectNeighborsRef = useRef(getObjectNeighbors)
getObjectNeighborsRef.current = getObjectNeighbors getObjectNeighborsRef.current = getObjectNeighbors
@ -114,15 +115,21 @@ const ObjectTableNavigationButtons = ({
{showEndingDivider && ( {showEndingDivider && (
<Divider type='vertical' style={{ margin: '0 4px', height: '18px' }} /> <Divider type='vertical' style={{ margin: '0 4px', height: '18px' }} />
)} )}
<Space size='small'> <Space.Compact size='small' style={{ height: '22px' }}>
<KeyboardShortcut <KeyboardShortcut
shortcut='alt+arrowleft' shortcut='alt+arrowleft'
hint='ALT ←' hint='ALT ←'
onTrigger={handlePreviousShortcut} onTrigger={handlePreviousShortcut}
> >
<Button <Button
icon={<ArrowLeftIcon />} type='text'
icon={
<ChevronLeftIcon
style={{ fontSize: '10px', marginBottom: '6px' }}
/>
}
onClick={handlePrevious} onClick={handlePrevious}
style={{ padding: 0, height: '22px', width: '32px' }}
disabled={disabled || loading || !neighbors.previous?._id} disabled={disabled || loading || !neighbors.previous?._id}
/> />
</KeyboardShortcut> </KeyboardShortcut>
@ -132,12 +139,18 @@ const ObjectTableNavigationButtons = ({
onTrigger={handleNextShortcut} onTrigger={handleNextShortcut}
> >
<Button <Button
icon={<ArrowRightIcon />} type='text'
icon={
<ChevronRightIcon
style={{ fontSize: '10px', marginBottom: '6px' }}
/>
}
onClick={handleNext} onClick={handleNext}
style={{ padding: 0, height: '22px', width: '32px' }}
disabled={disabled || loading || !neighbors.next?._id} disabled={disabled || loading || !neighbors.next?._id}
/> />
</KeyboardShortcut> </KeyboardShortcut>
</Space> </Space.Compact>
</Flex> </Flex>
) )
} }

View File

@ -7,6 +7,16 @@ const mergeHandler = (original, next) => (event) => {
original?.(event) original?.(event)
} }
const mergeRefs = (...refs) => (node) => {
refs.forEach((ref) => {
if (typeof ref === 'function') {
ref(node)
} else if (ref) {
ref.current = node
}
})
}
const Tooltip = ({ const Tooltip = ({
children, children,
title, title,
@ -17,7 +27,7 @@ const Tooltip = ({
const { showTooltip, hideTooltip } = useTooltipContext() const { showTooltip, hideTooltip } = useTooltipContext()
const id = useId() const id = useId()
const tooltipContent = title ?? content const tooltipContent = title ?? content
const spanRef = useRef(null) const childRef = useRef(null)
const hoveredTargetsRef = useRef(new Set()) const hoveredTargetsRef = useRef(new Set())
const hoveringRef = useRef(false) const hoveringRef = useRef(false)
const tooltipContentRef = useRef(tooltipContent) const tooltipContentRef = useRef(tooltipContent)
@ -60,8 +70,8 @@ const Tooltip = ({
onMouseLeaveRef.current = onMouseLeave onMouseLeaveRef.current = onMouseLeave
useEffect(() => { useEffect(() => {
const span = spanRef.current const child = childRef.current
if (!span || listenParents < 1) return if (!child || listenParents < 1) return
const hoveredTargets = hoveredTargetsRef.current const hoveredTargets = hoveredTargetsRef.current
@ -69,7 +79,7 @@ const Tooltip = ({
const handleLeave = (event) => onMouseLeaveRef.current(event) const handleLeave = (event) => onMouseLeaveRef.current(event)
const parents = [] const parents = []
let current = span.parentElement let current = child.parentElement
for (let i = 0; i < listenParents && current; i += 1) { for (let i = 0; i < listenParents && current; i += 1) {
parents.push(current) parents.push(current)
current = current.parentElement current = current.parentElement
@ -89,30 +99,15 @@ const Tooltip = ({
} }
}, [listenParents]) }, [listenParents])
const wrapWithSpan = (node) => ( if (!isValidElement(children)) {
<span ref={spanRef} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}> return children
{node}
</span>
)
if (isValidElement(children)) {
const trigger = cloneElement(children, {
onMouseEnter: mergeHandler(children.props.onMouseEnter, onMouseEnter),
onMouseLeave: mergeHandler(children.props.onMouseLeave, onMouseLeave)
})
if (listenParents > 0) {
return wrapWithSpan(children)
}
if (children.props.disabled) {
return wrapWithSpan(trigger)
}
return trigger
} }
return wrapWithSpan(children) return cloneElement(children, {
ref: mergeRefs(children.ref, childRef),
onMouseEnter: mergeHandler(children.props.onMouseEnter, onMouseEnter),
onMouseLeave: mergeHandler(children.props.onMouseLeave, onMouseLeave)
})
} }
Tooltip.propTypes = { Tooltip.propTypes = {

View File

@ -0,0 +1,78 @@
import {
createContext,
useCallback,
useContext,
useEffect,
useRef,
useState
} from 'react'
import PropTypes from 'prop-types'
const DashboardObjectToolsContext = createContext()
export const DashboardObjectToolsProvider = ({ children }) => {
const [currentObjectTools, setCurrentObjectToolsState] = useState(null)
const ownerRef = useRef(null)
const setCurrentObjectTools = useCallback((tools, ownerId) => {
ownerRef.current = ownerId
setCurrentObjectToolsState(tools)
}, [])
const clearCurrentObjectTools = useCallback((ownerId) => {
if (ownerRef.current !== ownerId) {
return
}
ownerRef.current = null
setCurrentObjectToolsState(null)
}, [])
return (
<DashboardObjectToolsContext.Provider
value={{
currentObjectTools,
setCurrentObjectTools,
clearCurrentObjectTools
}}
>
{children}
</DashboardObjectToolsContext.Provider>
)
}
DashboardObjectToolsProvider.propTypes = {
children: PropTypes.node.isRequired
}
// eslint-disable-next-line react-refresh/only-export-components
export const useDashboardObjectToolsContext = () => {
const context = useContext(DashboardObjectToolsContext)
if (!context) {
throw new Error(
'useDashboardObjectToolsContext must be used within a DashboardObjectToolsProvider'
)
}
return context
}
// eslint-disable-next-line react-refresh/only-export-components
export const useDashboardObjectTools = (tools) => {
const { setCurrentObjectTools, clearCurrentObjectTools } =
useDashboardObjectToolsContext()
const ownerIdRef = useRef(null)
if (ownerIdRef.current == null) {
ownerIdRef.current = Symbol('dashboardObjectTools')
}
useEffect(() => {
setCurrentObjectTools(tools, ownerIdRef.current)
}, [tools, setCurrentObjectTools])
useEffect(() => {
const ownerId = ownerIdRef.current
return () => clearCurrentObjectTools(ownerId)
}, [clearCurrentObjectTools])
}
export { DashboardObjectToolsContext }

View File

@ -0,0 +1,6 @@
import Icon from '@ant-design/icons'
import CustomIconSvg from '../../../assets/icons/chevronlefticon.svg?react'
const ChevronLeftIcon = (props) => <Icon component={CustomIconSvg} {...props} />
export default ChevronLeftIcon