Implement Tooltip System and Enhance UI Components

- Introduced a new Tooltip component to standardize tooltip functionality across the application, improving consistency and usability.
- Refactored existing components to utilize the new Tooltip implementation, enhancing user interactions with clearer feedback.
- Added a CursorTooltip for dynamic tooltip positioning based on mouse movement, improving user experience.
- Updated CSS styles for tooltips and related components to enhance visual clarity and responsiveness.
- Integrated TooltipProvider to manage tooltip state and visibility, streamlining tooltip management across the application.
This commit is contained in:
Tom Butcher 2026-08-20 20:14:51 +01:00
parent 0a09905f9f
commit 8ac04ac0bc
16 changed files with 419 additions and 88 deletions

View File

@ -361,6 +361,11 @@ code {
padding: 8px !important; padding: 8px !important;
} }
.cursor-tooltip .ant-card {
box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.08),
0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 9px 28px 8px rgba(0, 0, 0, 0.05);
}
/* --- Start of src/index.css --- */ /* --- Start of src/index.css --- */
body { body {
margin: 0; margin: 0;

View File

@ -30,6 +30,7 @@ import { ApiServerProvider } from './components/Dashboard/context/ApiServerConte
import { NotificationProvider } from './components/Dashboard/context/NotificationContext.jsx' import { NotificationProvider } from './components/Dashboard/context/NotificationContext.jsx'
import { ElectronProvider } from './components/Dashboard/context/ElectronContext.jsx' import { ElectronProvider } from './components/Dashboard/context/ElectronContext.jsx'
import { MessageProvider } from './components/Dashboard/context/MessageContext.jsx' import { MessageProvider } from './components/Dashboard/context/MessageContext.jsx'
import { TooltipProvider } from './components/Dashboard/context/TooltipContext.jsx'
import { AppUpdateProvider } from './components/Dashboard/context/AppUpdateContext.jsx' import { AppUpdateProvider } from './components/Dashboard/context/AppUpdateContext.jsx'
import AuthCallback from './components/App/AuthCallback.jsx' import AuthCallback from './components/App/AuthCallback.jsx'
import EmailNotificationTemplate from './components/Email/EmailNotificationTemplate.jsx' import EmailNotificationTemplate from './components/Email/EmailNotificationTemplate.jsx'
@ -85,13 +86,14 @@ const AppContent = () => {
<PrintServerProvider> <PrintServerProvider>
<ApiServerProvider> <ApiServerProvider>
<MessageProvider> <MessageProvider>
<TableStateProvider> <TooltipProvider>
<AppUpdateProvider> <TableStateProvider>
<NotificationProvider> <AppUpdateProvider>
<SpotlightProvider> <NotificationProvider>
<ActionsModalProvider> <SpotlightProvider>
<ActionsProvider> <ActionsModalProvider>
<Routes> <ActionsProvider>
<Routes>
<Route <Route
path='/applaunch' path='/applaunch'
element={<AuthLaunch />} element={<AuthLaunch />}
@ -162,12 +164,13 @@ const AppContent = () => {
} }
/> />
</Routes> </Routes>
</ActionsProvider> </ActionsProvider>
</ActionsModalProvider> </ActionsModalProvider>
</SpotlightProvider> </SpotlightProvider>
</NotificationProvider> </NotificationProvider>
</AppUpdateProvider> </AppUpdateProvider>
</TableStateProvider> </TableStateProvider>
</TooltipProvider>
</MessageProvider> </MessageProvider>
</ApiServerProvider> </ApiServerProvider>
</PrintServerProvider> </PrintServerProvider>

View File

@ -1,6 +1,7 @@
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { Button, Tooltip } from 'antd' import { Button } from 'antd'
import { useMessageContext } from '../context/MessageContext' import { useMessageContext } from '../context/MessageContext'
import Tooltip from './Tooltip'
import CopyIcon from '../../Icons/CopyIcon' import CopyIcon from '../../Icons/CopyIcon'
const CopyButton = ({ const CopyButton = ({
@ -49,7 +50,7 @@ const CopyButton = ({
return ( return (
<> <>
<Tooltip title={tooltip} arrow={false}> <Tooltip title={tooltip}>
<Button <Button
icon={<CopyIcon />} icon={<CopyIcon />}
style={{ minWidth: 25 }} style={{ minWidth: 25 }}

View File

@ -0,0 +1,142 @@
import { useCallback, useLayoutEffect, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import { Card } from 'antd'
import PropTypes from 'prop-types'
const OFFSET = 10
const getTooltipRoot = () => {
let root = document.getElementById('cursor-tooltip-root')
if (!root) {
root = document.createElement('div')
root.id = 'cursor-tooltip-root'
document.documentElement.appendChild(root)
}
return root
}
const CursorTooltip = ({ content, pointRef }) => {
const tooltipRef = useRef(null)
const sizeRef = useRef({ width: 0, height: 0 })
const frameRef = useRef(null)
const contentRef = useRef(content)
const lastContentRef = useRef(content)
const [root, setRoot] = useState(null)
contentRef.current = content
if (content != null && content !== false && content !== '') {
lastContentRef.current = content
}
const applyPosition = useCallback(() => {
const el = tooltipRef.current
if (!el) return
const { width, height } = sizeRef.current
const { x, y } = pointRef.current
let left = x + OFFSET
let top = y + OFFSET
const maxLeft = window.innerWidth - width - OFFSET
const maxTop = window.innerHeight - height - OFFSET
if (width > 0 && left > maxLeft) left = Math.max(OFFSET, maxLeft)
if (height > 0 && top > maxTop) top = Math.max(OFFSET, maxTop)
el.style.transform = `translate3d(${left}px, ${top}px, 0)`
}, [pointRef])
const schedulePosition = useCallback(() => {
if (frameRef.current != null) return
frameRef.current = window.requestAnimationFrame(() => {
frameRef.current = null
applyPosition()
})
}, [applyPosition])
useLayoutEffect(() => {
setRoot(getTooltipRoot())
}, [])
useLayoutEffect(() => {
applyPosition()
}, [applyPosition, content])
useLayoutEffect(() => {
const el = tooltipRef.current
if (!el) return
const updateSize = () => {
sizeRef.current = {
width: el.offsetWidth,
height: el.offsetHeight
}
applyPosition()
}
updateSize()
const observer = new ResizeObserver(updateSize)
observer.observe(el)
const onMove = () => {
const next = contentRef.current
if (next == null || next === false || next === '') return
schedulePosition()
}
window.addEventListener('mousemove', onMove, { passive: true })
return () => {
observer.disconnect()
window.removeEventListener('mousemove', onMove)
if (frameRef.current != null) {
window.cancelAnimationFrame(frameRef.current)
frameRef.current = null
}
}
}, [applyPosition, root, schedulePosition])
const visible = content != null && content !== false && content !== ''
if (!root) return null
return createPortal(
<div
ref={tooltipRef}
className='cursor-tooltip'
style={{
position: 'fixed',
top: 0,
left: 0,
zIndex: 1100,
pointerEvents: 'none',
visibility: visible ? 'visible' : 'hidden',
opacity: visible ? 1 : 0,
willChange: 'transform'
}}
>
<Card
size='small'
styles={{ body: { padding: '4px 10px' } }}
style={{
maxWidth: 360,
borderRadius: 10,
backgroundColor: 'var(--layout-modal-bg)'
}}
>
{lastContentRef.current}
</Card>
</div>,
root
)
}
CursorTooltip.propTypes = {
content: PropTypes.node,
pointRef: PropTypes.shape({
current: PropTypes.shape({
x: PropTypes.number,
y: PropTypes.number
})
}).isRequired
}
export default CursorTooltip

View File

@ -6,7 +6,6 @@ import {
Tag, Tag,
Space, Space,
Button, Button,
Tooltip,
Badge, Badge,
Divider, Divider,
Typography, Typography,
@ -17,6 +16,7 @@ import { AuthContext } from '../context/AuthContext'
import { SpotlightContext } from '../context/SpotlightContext' import { SpotlightContext } from '../context/SpotlightContext'
import { ApiServerContext } from '../context/ApiServerContext' import { ApiServerContext } from '../context/ApiServerContext'
import { NotificationContext } from '../context/NotificationContext' import { NotificationContext } from '../context/NotificationContext'
import Tooltip from './Tooltip'
import { useNavigate, useLocation } from 'react-router-dom' import { useNavigate, useLocation } from 'react-router-dom'
import { Header } from 'antd/es/layout/layout' import { Header } from 'antd/es/layout/layout'
import { useMediaQuery } from 'react-responsive' import { useMediaQuery } from 'react-responsive'
@ -43,7 +43,8 @@ import WindowAppMenu from './WindowAppMenu'
import WebAppSwitcher from './WebAppSwitcher' import WebAppSwitcher from './WebAppSwitcher'
import { import {
getSidebarDefaultPath, getSidebarDefaultPath,
getSidebarMenuSections getSidebarMenuSections,
isSidebarSectionVisible
} from '../../../database/Sidebars' } from '../../../database/Sidebars'
import { useAppUpdateContext } from '../context/AppUpdateContext' import { useAppUpdateContext } from '../context/AppUpdateContext'
@ -75,40 +76,44 @@ const DashboardNavigation = () => {
isMaximized isMaximized
} = useContext(ElectronContext) } = useContext(ElectronContext)
const { availableUpdate, checkForUpdates } = useAppUpdateContext() const { availableUpdate, checkForUpdates } = useAppUpdateContext()
const includeDev = import.meta.env.DEV
const mainMenuItems = useMemo( const mainMenuItems = useMemo(
() => [ () =>
{ [
key: 'production', {
label: 'Production', key: 'production',
className: 'electrobun-webkit-app-region-no-drag', label: 'Production',
icon: <ProductionIcon /> className: 'electrobun-webkit-app-region-no-drag',
}, icon: <ProductionIcon />
{ },
key: 'inventory', {
label: 'Inventory', key: 'inventory',
className: 'electrobun-webkit-app-region-no-drag', label: 'Inventory',
icon: <InventoryIcon /> className: 'electrobun-webkit-app-region-no-drag',
}, icon: <InventoryIcon />
{ },
key: 'sales', {
label: 'Sales', key: 'sales',
className: 'electrobun-webkit-app-region-no-drag', label: 'Sales',
icon: <SalesIcon /> className: 'electrobun-webkit-app-region-no-drag',
}, icon: <SalesIcon />
{ },
key: 'finance', {
label: 'Finance', key: 'finance',
className: 'electrobun-webkit-app-region-no-drag', label: 'Finance',
icon: <FinanceIcon /> className: 'electrobun-webkit-app-region-no-drag',
}, icon: <FinanceIcon />
{ },
key: 'management', {
label: 'Management', key: 'management',
className: 'electrobun-webkit-app-region-no-drag', label: 'Management',
icon: <SettingsIcon /> className: 'electrobun-webkit-app-region-no-drag',
} icon: <SettingsIcon />
], }
[] ].filter((item) =>
isSidebarSectionVisible(item.key, { includeDev, userProfile })
),
[includeDev, userProfile]
) )
const [userPopoverOpen, setUserPopoverOpen] = useState(false) const [userPopoverOpen, setUserPopoverOpen] = useState(false)
@ -120,10 +125,13 @@ const DashboardNavigation = () => {
useEffect(() => { useEffect(() => {
const pathParts = location.pathname.split('/').filter(Boolean) const pathParts = location.pathname.split('/').filter(Boolean)
if (pathParts.length > 2) { if (pathParts.length > 2) {
setSelectedMenuItem( const nextItem =
mainMenuItems.filter((item) => item.key == pathParts[1])[0] mainMenuItems.find((item) => item.key == pathParts[1]) ||
) mainMenuItems[0]
setSelectedKey(pathParts[1]) // Return the section (production/management) if (nextItem) {
setSelectedMenuItem(nextItem)
setSelectedKey(nextItem.key)
}
} }
}, [location.pathname, mainMenuItems]) }, [location.pathname, mainMenuItems])
@ -138,15 +146,14 @@ const DashboardNavigation = () => {
}, [connecting, connected]) }, [connecting, connected])
const handleMainMenuClick = ({ key }) => { const handleMainMenuClick = ({ key }) => {
navigate(getSidebarDefaultPath(key, { includeDev: import.meta.env.DEV })) navigate(getSidebarDefaultPath(key, { includeDev, userProfile }))
} }
useEffect(() => { useEffect(() => {
if (!isElectron || !setSidebarViewMenu) return if (!isElectron || !setSidebarViewMenu) return
const includeDev = import.meta.env.DEV const sections = getSidebarMenuSections({ includeDev, userProfile })
const sections = getSidebarMenuSections({ includeDev })
setSidebarViewMenu(sections) setSidebarViewMenu(sections)
}, [isElectron, setSidebarViewMenu]) }, [includeDev, isElectron, setSidebarViewMenu, userProfile])
const isMacOSApp = isElectron && platform == 'darwin' const isMacOSApp = isElectron && platform == 'darwin'
const isOtherApp = isElectron && platform != 'darwin' const isOtherApp = isElectron && platform != 'darwin'
@ -194,7 +201,7 @@ const DashboardNavigation = () => {
maxWidth: '100%' // responsive maxWidth: '100%' // responsive
}} }}
> >
{selectedMenuItem.icon} {selectedMenuItem?.icon}
<ElipsisText <ElipsisText
style={{ style={{
minWidth: 0, minWidth: 0,
@ -204,7 +211,7 @@ const DashboardNavigation = () => {
flex: 1 flex: 1
}} }}
> >
{selectedMenuItem.label} {selectedMenuItem?.label}
</ElipsisText> </ElipsisText>
</Flex> </Flex>
)} )}
@ -292,7 +299,7 @@ const DashboardNavigation = () => {
{import.meta.env.MODE === 'development' && ( {import.meta.env.MODE === 'development' && (
<Space> <Space>
{apiServerState === 'connected' ? ( {apiServerState === 'connected' ? (
<Tooltip title='Connected to api server' arrow={false}> <Tooltip content='Connected to api server'>
<Tag <Tag
color='success' color='success'
style={{ marginRight: 0 }} style={{ marginRight: 0 }}
@ -301,7 +308,7 @@ const DashboardNavigation = () => {
</Tooltip> </Tooltip>
) : null} ) : null}
{apiServerState === 'connecting' ? ( {apiServerState === 'connecting' ? (
<Tooltip title='Connecting to api erver...' arrow={false}> <Tooltip content='Connecting to api erver...'>
<Tag <Tag
color='warning' color='warning'
style={{ marginRight: 0 }} style={{ marginRight: 0 }}
@ -310,7 +317,7 @@ const DashboardNavigation = () => {
</Tooltip> </Tooltip>
) : null} ) : null}
{apiServerState === 'disconnected' ? ( {apiServerState === 'disconnected' ? (
<Tooltip title='Disconnected from api server' arrow={false}> <Tooltip content='Disconnected from api server'>
<Tag <Tag
color='error' color='error'
style={{ marginRight: 0 }} style={{ marginRight: 0 }}
@ -318,7 +325,7 @@ const DashboardNavigation = () => {
/> />
</Tooltip> </Tooltip>
) : null} ) : null}
<Tooltip title='Developer' arrow={false}> <Tooltip content='Developer'>
<Tag <Tag
color='yellow' color='yellow'
style={{ marginRight: 0 }} style={{ marginRight: 0 }}

View File

@ -7,9 +7,11 @@ import { useMediaQuery } from 'react-responsive'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { ElectronContext } from '../context/ElectronContext' import { ElectronContext } from '../context/ElectronContext'
import { AuthContext } from '../context/AuthContext'
import { getSidebarIconNode } from '../../Icons/sidebarIconMap' import { getSidebarIconNode } from '../../Icons/sidebarIconMap'
import SimpleBar from 'simplebar-react' import SimpleBar from 'simplebar-react'
import { useThemeContext } from '../context/ThemeContext' import { useThemeContext } from '../context/ThemeContext'
import { filterSidebarItemsByListPermission } from '../../../database/Sidebars'
const { Sider } = Layout const { Sider } = Layout
const DashboardSidebar = ({ const DashboardSidebar = ({
@ -28,6 +30,8 @@ const DashboardSidebar = ({
const navigate = useNavigate() const navigate = useNavigate()
const { isDarkMode } = useThemeContext() const { isDarkMode } = useThemeContext()
const { isElectron } = useContext(ElectronContext) const { isElectron } = useContext(ElectronContext)
const { userProfile } = useContext(AuthContext)
const allowedItems = filterSidebarItemsByListPermission(items, userProfile)
useEffect(() => { useEffect(() => {
if (typeof collapsedProp === 'boolean') { if (typeof collapsedProp === 'boolean') {
@ -69,7 +73,7 @@ const DashboardSidebar = ({
} }
// Map items recursively // Map items recursively
const _items = mapItemsRecursively(items) const _items = mapItemsRecursively(allowedItems)
if (isMobile) { if (isMobile) {
return ( return (

View File

@ -1,5 +1,6 @@
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { Typography, Flex, Button, Tooltip } from 'antd' import { Typography, Flex, Button } from 'antd'
import Tooltip from './Tooltip'
import NewMailIcon from '../../Icons/NewMailIcon' import NewMailIcon from '../../Icons/NewMailIcon'
// import CopyIcon from './CopyIcon' // import CopyIcon from './CopyIcon'
import CopyButton from './CopyButton' import CopyButton from './CopyButton'
@ -22,7 +23,7 @@ const EmailDisplay = ({ email, showCopy = true, showLink = false }) => {
<ElipsisText style={{ marginRight: 8 }}> <ElipsisText style={{ marginRight: 8 }}>
{email} {email}
</ElipsisText> </ElipsisText>
<Tooltip title='Email' arrow={false}> <Tooltip title='Email'>
<Button <Button
icon={<NewMailIcon />} icon={<NewMailIcon />}
type='text' type='text'

View File

@ -9,10 +9,10 @@ import {
Input, Input,
Popover, Popover,
Select, Select,
Space, Space
Tooltip
} from 'antd' } from 'antd'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import Tooltip from './Tooltip'
import BlockquoteIcon from '../../Icons/BlockquoteIcon' import BlockquoteIcon from '../../Icons/BlockquoteIcon'
import BoldIcon from '../../Icons/BoldIcon' import BoldIcon from '../../Icons/BoldIcon'
import BulletListIcon from '../../Icons/BulletListIcon' import BulletListIcon from '../../Icons/BulletListIcon'
@ -199,7 +199,7 @@ const MarkdownToolbar = ({
disabled={!editor || editingToolsDisabled} disabled={!editor || editingToolsDisabled}
/> />
<Space.Compact size='small'> <Space.Compact size='small'>
<Tooltip title='Bold' arrow={false}> <Tooltip content='Bold'>
<Button <Button
size={size} size={size}
type={editorState.isBold ? 'primary' : 'default'} type={editorState.isBold ? 'primary' : 'default'}
@ -209,7 +209,7 @@ const MarkdownToolbar = ({
style={{ minWidth: 32 }} style={{ minWidth: 32 }}
/> />
</Tooltip> </Tooltip>
<Tooltip title='Italic' arrow={false}> <Tooltip content='Italic'>
<Button <Button
size={size} size={size}
type={editorState.isItalic ? 'primary' : 'default'} type={editorState.isItalic ? 'primary' : 'default'}
@ -219,7 +219,7 @@ const MarkdownToolbar = ({
style={{ minWidth: 32 }} style={{ minWidth: 32 }}
/> />
</Tooltip> </Tooltip>
<Tooltip title='Underline' arrow={false}> <Tooltip content='Underline'>
<Button <Button
size={size} size={size}
type={editorState.isUnderline ? 'primary' : 'default'} type={editorState.isUnderline ? 'primary' : 'default'}
@ -231,7 +231,7 @@ const MarkdownToolbar = ({
</Tooltip> </Tooltip>
</Space.Compact> </Space.Compact>
<Space.Compact size='small'> <Space.Compact size='small'>
<Tooltip title='Bulleted list' arrow={false}> <Tooltip content='Bulleted list'>
<Button <Button
size={size} size={size}
type={editorState.isBulletList ? 'primary' : 'default'} type={editorState.isBulletList ? 'primary' : 'default'}
@ -241,7 +241,7 @@ const MarkdownToolbar = ({
style={{ minWidth: 32 }} style={{ minWidth: 32 }}
/> />
</Tooltip> </Tooltip>
<Tooltip title='Numbered list' arrow={false}> <Tooltip content='Numbered list'>
<Button <Button
size={size} size={size}
type={editorState.isOrderedList ? 'primary' : 'default'} type={editorState.isOrderedList ? 'primary' : 'default'}
@ -252,7 +252,7 @@ const MarkdownToolbar = ({
/> />
</Tooltip> </Tooltip>
</Space.Compact> </Space.Compact>
<Tooltip title='Block quote' arrow={false}> <Tooltip content='Block quote'>
<Button <Button
size={size} size={size}
type={editorState.blockTypeValue === 'quote' ? 'primary' : 'default'} type={editorState.blockTypeValue === 'quote' ? 'primary' : 'default'}
@ -275,7 +275,7 @@ const MarkdownToolbar = ({
styles={{ body: { borderRadius: '22px' } }} styles={{ body: { borderRadius: '22px' } }}
onOpenChange={handleLinkPopoverOpenChange} onOpenChange={handleLinkPopoverOpenChange}
> >
<Tooltip title='Insert link' arrow={false}> <Tooltip content='Insert link'>
<Button <Button
size={size} size={size}
icon={<LinkIcon />} icon={<LinkIcon />}
@ -284,7 +284,7 @@ const MarkdownToolbar = ({
/> />
</Tooltip> </Tooltip>
</Popover> </Popover>
<Tooltip title='Insert code block' arrow={false}> <Tooltip content='Insert code block'>
<Button <Button
size={size} size={size}
icon={<JsonObjectIcon />} icon={<JsonObjectIcon />}
@ -293,7 +293,7 @@ const MarkdownToolbar = ({
style={{ minWidth: 32 }} style={{ minWidth: 32 }}
/> />
</Tooltip> </Tooltip>
<Tooltip title='Insert horizontal rule' arrow={false}> <Tooltip content='Insert horizontal rule'>
<Button <Button
size={size} size={size}
icon={<MinusOutlined />} icon={<MinusOutlined />}

View File

@ -19,7 +19,6 @@ import {
Spin, Spin,
Button, Button,
Space, Space,
Tooltip,
Form, Form,
Splitter, Splitter,
Card Card
@ -56,6 +55,7 @@ import {
useTableStatePersistence useTableStatePersistence
} from '../context/TableStateContext' } from '../context/TableStateContext'
import { hasActionPermission } from '../../../database/permissions' import { hasActionPermission } from '../../../database/permissions'
import Tooltip from './Tooltip'
const logger = loglevel.getLogger('DasboardTable') const logger = loglevel.getLogger('DasboardTable')
logger.setLevel(config.logLevel) logger.setLevel(config.logLevel)
@ -448,7 +448,7 @@ const ObjectTable = forwardRef(
} }
} }
return ( return (
<Tooltip key={index} title={action.label} arrow={false}> <Tooltip key={index} title={action.label}>
<Button <Button
icon={ icon={
action.icon ? ( action.icon ? (

View File

@ -9,7 +9,8 @@ import {
useLayoutEffect useLayoutEffect
} from 'react' } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { Table, Flex, Tooltip, Button } from 'antd' import { Table, Flex, Button } from 'antd'
import Tooltip from './Tooltip'
import { useMediaQuery } from 'react-responsive' import { useMediaQuery } from 'react-responsive'
import { ElectronContext } from '../context/ElectronContext' import { ElectronContext } from '../context/ElectronContext'
import { import {
@ -540,7 +541,7 @@ const PermissionsMatrix = ({
const indeterminate = inherit && state === null const indeterminate = inherit && state === null
return ( return (
<Tooltip title={getCheckboxTitle(state, inherit)} arrow={false}> <Tooltip title={getCheckboxTitle(state, inherit)}>
<span> <span>
<PermissionCheckbox <PermissionCheckbox
checked={checked} checked={checked}

View File

@ -1,6 +1,7 @@
import { useState } from 'react' import { useState } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { Typography, Tooltip, Button } from 'antd' import { Typography, Button } from 'antd'
import Tooltip from './Tooltip'
import CopyButton from './CopyButton' import CopyButton from './CopyButton'
import EyeIcon from '../../Icons/EyeIcon' import EyeIcon from '../../Icons/EyeIcon'
import EyeSlashIcon from '../../Icons/EyeSlashIcon' import EyeSlashIcon from '../../Icons/EyeSlashIcon'
@ -20,7 +21,7 @@ const SecretDisplay = ({ value, reveal = false }) => {
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}> <span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
<Text code>{reveal && visible ? value : masked}</Text> <Text code>{reveal && visible ? value : masked}</Text>
{reveal && ( {reveal && (
<Tooltip title={visible ? 'Hide' : 'Show'} arrow={false}> <Tooltip title={visible ? 'Hide' : 'Show'}>
<Button <Button
type='text' type='text'
icon={visible ? <EyeSlashIcon /> : <EyeIcon />} icon={visible ? <EyeSlashIcon /> : <EyeIcon />}

View File

@ -0,0 +1,72 @@
import {
cloneElement,
isValidElement,
useEffect,
useId,
useRef
} from 'react'
import PropTypes from 'prop-types'
import { useTooltipContext } from '../context/TooltipContext'
const mergeHandler = (original, next) => (event) => {
next(event)
original?.(event)
}
const Tooltip = ({ children, title, content }) => {
const { showTooltip, hideTooltip } = useTooltipContext()
const id = useId()
const tooltipContent = title ?? content
const hoveringRef = useRef(false)
useEffect(() => {
return () => hideTooltip(id)
}, [hideTooltip, id])
useEffect(() => {
if (hoveringRef.current) {
showTooltip(id, tooltipContent)
}
}, [id, showTooltip, tooltipContent])
const onMouseEnter = (event) => {
hoveringRef.current = true
showTooltip(id, tooltipContent, event.clientX, event.clientY)
}
const onMouseLeave = () => {
hoveringRef.current = false
hideTooltip(id)
}
if (isValidElement(children)) {
const trigger = cloneElement(children, {
onMouseEnter: mergeHandler(children.props.onMouseEnter, onMouseEnter),
onMouseLeave: mergeHandler(children.props.onMouseLeave, onMouseLeave)
})
if (children.props.disabled) {
return (
<span onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
{trigger}
</span>
)
}
return trigger
}
return (
<span onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
{children}
</span>
)
}
Tooltip.propTypes = {
children: PropTypes.node.isRequired,
title: PropTypes.node,
content: PropTypes.node
}
export default Tooltip

View File

@ -1,5 +1,6 @@
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { Typography, Flex, Button, Tooltip } from 'antd' import { Typography, Flex, Button } from 'antd'
import Tooltip from './Tooltip'
import LinkIcon from '../../Icons/LinkIcon' import LinkIcon from '../../Icons/LinkIcon'
import CopyButton from './CopyButton' import CopyButton from './CopyButton'
import ElipsisText from './ElipsisText' import ElipsisText from './ElipsisText'
@ -34,7 +35,7 @@ const UrlDisplay = ({ url, showCopy = true, showLink = false }) => {
<ElipsisText style={{ marginRight: 8, minWidth: 0 }}> <ElipsisText style={{ marginRight: 8, minWidth: 0 }}>
{url} {url}
</ElipsisText> </ElipsisText>
<Tooltip title='Open URL' arrow={false}> <Tooltip title='Open URL'>
<Button <Button
icon={<LinkIcon />} icon={<LinkIcon />}
type='text' type='text'

View File

@ -3,6 +3,7 @@ import PropTypes from 'prop-types'
import { Button, Dropdown, Flex } from 'antd' import { Button, Dropdown, Flex } from 'antd'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { ElectronContext } from '../context/ElectronContext' import { ElectronContext } from '../context/ElectronContext'
import { AuthContext } from '../context/AuthContext'
import { useAppUpdateContext } from '../context/AppUpdateContext' import { useAppUpdateContext } from '../context/AppUpdateContext'
import { getSidebarMenuSections } from '../../../database/Sidebars' import { getSidebarMenuSections } from '../../../database/Sidebars'
import FarmControlLogoSmall from '../../Logos/FarmControlLogoSmall' import FarmControlLogoSmall from '../../Logos/FarmControlLogoSmall'
@ -75,13 +76,14 @@ MenuButton.propTypes = {
const WindowAppMenu = () => { const WindowAppMenu = () => {
const navigate = useNavigate() const navigate = useNavigate()
const { userProfile } = useContext(AuthContext)
const { handleWindowControl } = useContext(ElectronContext) const { handleWindowControl } = useContext(ElectronContext)
const { checkForUpdates } = useAppUpdateContext() const { checkForUpdates } = useAppUpdateContext()
const includeDev = import.meta.env.DEV const includeDev = import.meta.env.DEV
const viewSections = useMemo( const viewSections = useMemo(
() => getSidebarMenuSections({ includeDev }), () => getSidebarMenuSections({ includeDev, userProfile }),
[includeDev] [includeDev, userProfile]
) )
const appMenuItems = useMemo( const appMenuItems = useMemo(

View File

@ -120,6 +120,13 @@ const ActionsProvider = ({ children }) => {
} }
if (action.type === 'page') { if (action.type === 'page') {
if (action.name === 'list') {
lastHandledAction.current = actionKey
if (location.pathname !== model.url) {
navigate(model.url, { replace: true })
}
return
}
const pageName = action.pageName || action.name const pageName = action.pageName || action.name
const objectId = const objectId =
getObjectIdFromSearch(actionObjectType, location.search) || getObjectIdFromSearch(actionObjectType, location.search) ||

View File

@ -0,0 +1,84 @@
import {
createContext,
useCallback,
useContext,
useEffect,
useRef,
useState
} from 'react'
import PropTypes from 'prop-types'
import CursorTooltip from '../common/CursorTooltip'
const TooltipContext = createContext(null)
export const TooltipProvider = ({ children }) => {
const [content, setContent] = useState(null)
const stackRef = useRef([])
const hideTimerRef = useRef(null)
const pointRef = useRef({ x: 0, y: 0 })
useEffect(() => {
const onMove = (event) => {
pointRef.current = { x: event.clientX, y: event.clientY }
}
window.addEventListener('mousemove', onMove, { passive: true })
return () => window.removeEventListener('mousemove', onMove)
}, [])
const showTooltip = useCallback((id, nextContent, x, y) => {
if (nextContent == null || nextContent === false || nextContent === '') {
return
}
if (typeof x === 'number' && typeof y === 'number') {
pointRef.current = { x, y }
}
if (hideTimerRef.current != null) {
window.clearTimeout(hideTimerRef.current)
hideTimerRef.current = null
}
stackRef.current = [
...stackRef.current.filter((entry) => entry.id !== id),
{ id, content: nextContent }
]
setContent(nextContent)
}, [])
const hideTooltip = useCallback((id) => {
stackRef.current = stackRef.current.filter((entry) => entry.id !== id)
const last = stackRef.current[stackRef.current.length - 1]
if (hideTimerRef.current != null) {
window.clearTimeout(hideTimerRef.current)
}
hideTimerRef.current = window.setTimeout(() => {
hideTimerRef.current = null
setContent(last?.content ?? null)
}, 80)
}, [])
return (
<TooltipContext.Provider value={{ showTooltip, hideTooltip }}>
{children}
<CursorTooltip content={content} pointRef={pointRef} />
</TooltipContext.Provider>
)
}
TooltipProvider.propTypes = {
children: PropTypes.node.isRequired
}
// eslint-disable-next-line react-refresh/only-export-components
export const useTooltipContext = () => {
const context = useContext(TooltipContext)
if (!context) {
throw new Error('useTooltipContext must be used within a TooltipProvider')
}
return context
}
export { TooltipContext }