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:
parent
0a09905f9f
commit
8ac04ac0bc
@ -361,6 +361,11 @@ code {
|
||||
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 --- */
|
||||
body {
|
||||
margin: 0;
|
||||
|
||||
29
src/App.jsx
29
src/App.jsx
@ -30,6 +30,7 @@ import { ApiServerProvider } from './components/Dashboard/context/ApiServerConte
|
||||
import { NotificationProvider } from './components/Dashboard/context/NotificationContext.jsx'
|
||||
import { ElectronProvider } from './components/Dashboard/context/ElectronContext.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 AuthCallback from './components/App/AuthCallback.jsx'
|
||||
import EmailNotificationTemplate from './components/Email/EmailNotificationTemplate.jsx'
|
||||
@ -85,13 +86,14 @@ const AppContent = () => {
|
||||
<PrintServerProvider>
|
||||
<ApiServerProvider>
|
||||
<MessageProvider>
|
||||
<TableStateProvider>
|
||||
<AppUpdateProvider>
|
||||
<NotificationProvider>
|
||||
<SpotlightProvider>
|
||||
<ActionsModalProvider>
|
||||
<ActionsProvider>
|
||||
<Routes>
|
||||
<TooltipProvider>
|
||||
<TableStateProvider>
|
||||
<AppUpdateProvider>
|
||||
<NotificationProvider>
|
||||
<SpotlightProvider>
|
||||
<ActionsModalProvider>
|
||||
<ActionsProvider>
|
||||
<Routes>
|
||||
<Route
|
||||
path='/applaunch'
|
||||
element={<AuthLaunch />}
|
||||
@ -162,12 +164,13 @@ const AppContent = () => {
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</ActionsProvider>
|
||||
</ActionsModalProvider>
|
||||
</SpotlightProvider>
|
||||
</NotificationProvider>
|
||||
</AppUpdateProvider>
|
||||
</TableStateProvider>
|
||||
</ActionsProvider>
|
||||
</ActionsModalProvider>
|
||||
</SpotlightProvider>
|
||||
</NotificationProvider>
|
||||
</AppUpdateProvider>
|
||||
</TableStateProvider>
|
||||
</TooltipProvider>
|
||||
</MessageProvider>
|
||||
</ApiServerProvider>
|
||||
</PrintServerProvider>
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import { Button, Tooltip } from 'antd'
|
||||
import { Button } from 'antd'
|
||||
import { useMessageContext } from '../context/MessageContext'
|
||||
import Tooltip from './Tooltip'
|
||||
import CopyIcon from '../../Icons/CopyIcon'
|
||||
|
||||
const CopyButton = ({
|
||||
@ -49,7 +50,7 @@ const CopyButton = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip title={tooltip} arrow={false}>
|
||||
<Tooltip title={tooltip}>
|
||||
<Button
|
||||
icon={<CopyIcon />}
|
||||
style={{ minWidth: 25 }}
|
||||
|
||||
142
src/components/Dashboard/common/CursorTooltip.jsx
Normal file
142
src/components/Dashboard/common/CursorTooltip.jsx
Normal 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
|
||||
@ -6,7 +6,6 @@ import {
|
||||
Tag,
|
||||
Space,
|
||||
Button,
|
||||
Tooltip,
|
||||
Badge,
|
||||
Divider,
|
||||
Typography,
|
||||
@ -17,6 +16,7 @@ import { AuthContext } from '../context/AuthContext'
|
||||
import { SpotlightContext } from '../context/SpotlightContext'
|
||||
import { ApiServerContext } from '../context/ApiServerContext'
|
||||
import { NotificationContext } from '../context/NotificationContext'
|
||||
import Tooltip from './Tooltip'
|
||||
import { useNavigate, useLocation } from 'react-router-dom'
|
||||
import { Header } from 'antd/es/layout/layout'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
@ -43,7 +43,8 @@ import WindowAppMenu from './WindowAppMenu'
|
||||
import WebAppSwitcher from './WebAppSwitcher'
|
||||
import {
|
||||
getSidebarDefaultPath,
|
||||
getSidebarMenuSections
|
||||
getSidebarMenuSections,
|
||||
isSidebarSectionVisible
|
||||
} from '../../../database/Sidebars'
|
||||
|
||||
import { useAppUpdateContext } from '../context/AppUpdateContext'
|
||||
@ -75,40 +76,44 @@ const DashboardNavigation = () => {
|
||||
isMaximized
|
||||
} = useContext(ElectronContext)
|
||||
const { availableUpdate, checkForUpdates } = useAppUpdateContext()
|
||||
const includeDev = import.meta.env.DEV
|
||||
const mainMenuItems = useMemo(
|
||||
() => [
|
||||
{
|
||||
key: 'production',
|
||||
label: 'Production',
|
||||
className: 'electrobun-webkit-app-region-no-drag',
|
||||
icon: <ProductionIcon />
|
||||
},
|
||||
{
|
||||
key: 'inventory',
|
||||
label: 'Inventory',
|
||||
className: 'electrobun-webkit-app-region-no-drag',
|
||||
icon: <InventoryIcon />
|
||||
},
|
||||
{
|
||||
key: 'sales',
|
||||
label: 'Sales',
|
||||
className: 'electrobun-webkit-app-region-no-drag',
|
||||
icon: <SalesIcon />
|
||||
},
|
||||
{
|
||||
key: 'finance',
|
||||
label: 'Finance',
|
||||
className: 'electrobun-webkit-app-region-no-drag',
|
||||
icon: <FinanceIcon />
|
||||
},
|
||||
{
|
||||
key: 'management',
|
||||
label: 'Management',
|
||||
className: 'electrobun-webkit-app-region-no-drag',
|
||||
icon: <SettingsIcon />
|
||||
}
|
||||
],
|
||||
[]
|
||||
() =>
|
||||
[
|
||||
{
|
||||
key: 'production',
|
||||
label: 'Production',
|
||||
className: 'electrobun-webkit-app-region-no-drag',
|
||||
icon: <ProductionIcon />
|
||||
},
|
||||
{
|
||||
key: 'inventory',
|
||||
label: 'Inventory',
|
||||
className: 'electrobun-webkit-app-region-no-drag',
|
||||
icon: <InventoryIcon />
|
||||
},
|
||||
{
|
||||
key: 'sales',
|
||||
label: 'Sales',
|
||||
className: 'electrobun-webkit-app-region-no-drag',
|
||||
icon: <SalesIcon />
|
||||
},
|
||||
{
|
||||
key: 'finance',
|
||||
label: 'Finance',
|
||||
className: 'electrobun-webkit-app-region-no-drag',
|
||||
icon: <FinanceIcon />
|
||||
},
|
||||
{
|
||||
key: 'management',
|
||||
label: 'Management',
|
||||
className: 'electrobun-webkit-app-region-no-drag',
|
||||
icon: <SettingsIcon />
|
||||
}
|
||||
].filter((item) =>
|
||||
isSidebarSectionVisible(item.key, { includeDev, userProfile })
|
||||
),
|
||||
[includeDev, userProfile]
|
||||
)
|
||||
|
||||
const [userPopoverOpen, setUserPopoverOpen] = useState(false)
|
||||
@ -120,10 +125,13 @@ const DashboardNavigation = () => {
|
||||
useEffect(() => {
|
||||
const pathParts = location.pathname.split('/').filter(Boolean)
|
||||
if (pathParts.length > 2) {
|
||||
setSelectedMenuItem(
|
||||
mainMenuItems.filter((item) => item.key == pathParts[1])[0]
|
||||
)
|
||||
setSelectedKey(pathParts[1]) // Return the section (production/management)
|
||||
const nextItem =
|
||||
mainMenuItems.find((item) => item.key == pathParts[1]) ||
|
||||
mainMenuItems[0]
|
||||
if (nextItem) {
|
||||
setSelectedMenuItem(nextItem)
|
||||
setSelectedKey(nextItem.key)
|
||||
}
|
||||
}
|
||||
}, [location.pathname, mainMenuItems])
|
||||
|
||||
@ -138,15 +146,14 @@ const DashboardNavigation = () => {
|
||||
}, [connecting, connected])
|
||||
|
||||
const handleMainMenuClick = ({ key }) => {
|
||||
navigate(getSidebarDefaultPath(key, { includeDev: import.meta.env.DEV }))
|
||||
navigate(getSidebarDefaultPath(key, { includeDev, userProfile }))
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!isElectron || !setSidebarViewMenu) return
|
||||
const includeDev = import.meta.env.DEV
|
||||
const sections = getSidebarMenuSections({ includeDev })
|
||||
const sections = getSidebarMenuSections({ includeDev, userProfile })
|
||||
setSidebarViewMenu(sections)
|
||||
}, [isElectron, setSidebarViewMenu])
|
||||
}, [includeDev, isElectron, setSidebarViewMenu, userProfile])
|
||||
|
||||
const isMacOSApp = isElectron && platform == 'darwin'
|
||||
const isOtherApp = isElectron && platform != 'darwin'
|
||||
@ -194,7 +201,7 @@ const DashboardNavigation = () => {
|
||||
maxWidth: '100%' // responsive
|
||||
}}
|
||||
>
|
||||
{selectedMenuItem.icon}
|
||||
{selectedMenuItem?.icon}
|
||||
<ElipsisText
|
||||
style={{
|
||||
minWidth: 0,
|
||||
@ -204,7 +211,7 @@ const DashboardNavigation = () => {
|
||||
flex: 1
|
||||
}}
|
||||
>
|
||||
{selectedMenuItem.label}
|
||||
{selectedMenuItem?.label}
|
||||
</ElipsisText>
|
||||
</Flex>
|
||||
)}
|
||||
@ -292,7 +299,7 @@ const DashboardNavigation = () => {
|
||||
{import.meta.env.MODE === 'development' && (
|
||||
<Space>
|
||||
{apiServerState === 'connected' ? (
|
||||
<Tooltip title='Connected to api server' arrow={false}>
|
||||
<Tooltip content='Connected to api server'>
|
||||
<Tag
|
||||
color='success'
|
||||
style={{ marginRight: 0 }}
|
||||
@ -301,7 +308,7 @@ const DashboardNavigation = () => {
|
||||
</Tooltip>
|
||||
) : null}
|
||||
{apiServerState === 'connecting' ? (
|
||||
<Tooltip title='Connecting to api erver...' arrow={false}>
|
||||
<Tooltip content='Connecting to api erver...'>
|
||||
<Tag
|
||||
color='warning'
|
||||
style={{ marginRight: 0 }}
|
||||
@ -310,7 +317,7 @@ const DashboardNavigation = () => {
|
||||
</Tooltip>
|
||||
) : null}
|
||||
{apiServerState === 'disconnected' ? (
|
||||
<Tooltip title='Disconnected from api server' arrow={false}>
|
||||
<Tooltip content='Disconnected from api server'>
|
||||
<Tag
|
||||
color='error'
|
||||
style={{ marginRight: 0 }}
|
||||
@ -318,7 +325,7 @@ const DashboardNavigation = () => {
|
||||
/>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
<Tooltip title='Developer' arrow={false}>
|
||||
<Tooltip content='Developer'>
|
||||
<Tag
|
||||
color='yellow'
|
||||
style={{ marginRight: 0 }}
|
||||
|
||||
@ -7,9 +7,11 @@ import { useMediaQuery } from 'react-responsive'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import PropTypes from 'prop-types'
|
||||
import { ElectronContext } from '../context/ElectronContext'
|
||||
import { AuthContext } from '../context/AuthContext'
|
||||
import { getSidebarIconNode } from '../../Icons/sidebarIconMap'
|
||||
import SimpleBar from 'simplebar-react'
|
||||
import { useThemeContext } from '../context/ThemeContext'
|
||||
import { filterSidebarItemsByListPermission } from '../../../database/Sidebars'
|
||||
const { Sider } = Layout
|
||||
|
||||
const DashboardSidebar = ({
|
||||
@ -28,6 +30,8 @@ const DashboardSidebar = ({
|
||||
const navigate = useNavigate()
|
||||
const { isDarkMode } = useThemeContext()
|
||||
const { isElectron } = useContext(ElectronContext)
|
||||
const { userProfile } = useContext(AuthContext)
|
||||
const allowedItems = filterSidebarItemsByListPermission(items, userProfile)
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof collapsedProp === 'boolean') {
|
||||
@ -69,7 +73,7 @@ const DashboardSidebar = ({
|
||||
}
|
||||
|
||||
// Map items recursively
|
||||
const _items = mapItemsRecursively(items)
|
||||
const _items = mapItemsRecursively(allowedItems)
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
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 CopyIcon from './CopyIcon'
|
||||
import CopyButton from './CopyButton'
|
||||
@ -22,7 +23,7 @@ const EmailDisplay = ({ email, showCopy = true, showLink = false }) => {
|
||||
<ElipsisText style={{ marginRight: 8 }}>
|
||||
{email}
|
||||
</ElipsisText>
|
||||
<Tooltip title='Email' arrow={false}>
|
||||
<Tooltip title='Email'>
|
||||
<Button
|
||||
icon={<NewMailIcon />}
|
||||
type='text'
|
||||
|
||||
@ -9,10 +9,10 @@ import {
|
||||
Input,
|
||||
Popover,
|
||||
Select,
|
||||
Space,
|
||||
Tooltip
|
||||
Space
|
||||
} from 'antd'
|
||||
import PropTypes from 'prop-types'
|
||||
import Tooltip from './Tooltip'
|
||||
import BlockquoteIcon from '../../Icons/BlockquoteIcon'
|
||||
import BoldIcon from '../../Icons/BoldIcon'
|
||||
import BulletListIcon from '../../Icons/BulletListIcon'
|
||||
@ -199,7 +199,7 @@ const MarkdownToolbar = ({
|
||||
disabled={!editor || editingToolsDisabled}
|
||||
/>
|
||||
<Space.Compact size='small'>
|
||||
<Tooltip title='Bold' arrow={false}>
|
||||
<Tooltip content='Bold'>
|
||||
<Button
|
||||
size={size}
|
||||
type={editorState.isBold ? 'primary' : 'default'}
|
||||
@ -209,7 +209,7 @@ const MarkdownToolbar = ({
|
||||
style={{ minWidth: 32 }}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title='Italic' arrow={false}>
|
||||
<Tooltip content='Italic'>
|
||||
<Button
|
||||
size={size}
|
||||
type={editorState.isItalic ? 'primary' : 'default'}
|
||||
@ -219,7 +219,7 @@ const MarkdownToolbar = ({
|
||||
style={{ minWidth: 32 }}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title='Underline' arrow={false}>
|
||||
<Tooltip content='Underline'>
|
||||
<Button
|
||||
size={size}
|
||||
type={editorState.isUnderline ? 'primary' : 'default'}
|
||||
@ -231,7 +231,7 @@ const MarkdownToolbar = ({
|
||||
</Tooltip>
|
||||
</Space.Compact>
|
||||
<Space.Compact size='small'>
|
||||
<Tooltip title='Bulleted list' arrow={false}>
|
||||
<Tooltip content='Bulleted list'>
|
||||
<Button
|
||||
size={size}
|
||||
type={editorState.isBulletList ? 'primary' : 'default'}
|
||||
@ -241,7 +241,7 @@ const MarkdownToolbar = ({
|
||||
style={{ minWidth: 32 }}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title='Numbered list' arrow={false}>
|
||||
<Tooltip content='Numbered list'>
|
||||
<Button
|
||||
size={size}
|
||||
type={editorState.isOrderedList ? 'primary' : 'default'}
|
||||
@ -252,7 +252,7 @@ const MarkdownToolbar = ({
|
||||
/>
|
||||
</Tooltip>
|
||||
</Space.Compact>
|
||||
<Tooltip title='Block quote' arrow={false}>
|
||||
<Tooltip content='Block quote'>
|
||||
<Button
|
||||
size={size}
|
||||
type={editorState.blockTypeValue === 'quote' ? 'primary' : 'default'}
|
||||
@ -275,7 +275,7 @@ const MarkdownToolbar = ({
|
||||
styles={{ body: { borderRadius: '22px' } }}
|
||||
onOpenChange={handleLinkPopoverOpenChange}
|
||||
>
|
||||
<Tooltip title='Insert link' arrow={false}>
|
||||
<Tooltip content='Insert link'>
|
||||
<Button
|
||||
size={size}
|
||||
icon={<LinkIcon />}
|
||||
@ -284,7 +284,7 @@ const MarkdownToolbar = ({
|
||||
/>
|
||||
</Tooltip>
|
||||
</Popover>
|
||||
<Tooltip title='Insert code block' arrow={false}>
|
||||
<Tooltip content='Insert code block'>
|
||||
<Button
|
||||
size={size}
|
||||
icon={<JsonObjectIcon />}
|
||||
@ -293,7 +293,7 @@ const MarkdownToolbar = ({
|
||||
style={{ minWidth: 32 }}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title='Insert horizontal rule' arrow={false}>
|
||||
<Tooltip content='Insert horizontal rule'>
|
||||
<Button
|
||||
size={size}
|
||||
icon={<MinusOutlined />}
|
||||
|
||||
@ -19,7 +19,6 @@ import {
|
||||
Spin,
|
||||
Button,
|
||||
Space,
|
||||
Tooltip,
|
||||
Form,
|
||||
Splitter,
|
||||
Card
|
||||
@ -56,6 +55,7 @@ import {
|
||||
useTableStatePersistence
|
||||
} from '../context/TableStateContext'
|
||||
import { hasActionPermission } from '../../../database/permissions'
|
||||
import Tooltip from './Tooltip'
|
||||
|
||||
const logger = loglevel.getLogger('DasboardTable')
|
||||
logger.setLevel(config.logLevel)
|
||||
@ -448,7 +448,7 @@ const ObjectTable = forwardRef(
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Tooltip key={index} title={action.label} arrow={false}>
|
||||
<Tooltip key={index} title={action.label}>
|
||||
<Button
|
||||
icon={
|
||||
action.icon ? (
|
||||
|
||||
@ -9,7 +9,8 @@ import {
|
||||
useLayoutEffect
|
||||
} from 'react'
|
||||
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 { ElectronContext } from '../context/ElectronContext'
|
||||
import {
|
||||
@ -540,7 +541,7 @@ const PermissionsMatrix = ({
|
||||
const indeterminate = inherit && state === null
|
||||
|
||||
return (
|
||||
<Tooltip title={getCheckboxTitle(state, inherit)} arrow={false}>
|
||||
<Tooltip title={getCheckboxTitle(state, inherit)}>
|
||||
<span>
|
||||
<PermissionCheckbox
|
||||
checked={checked}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { useState } from 'react'
|
||||
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 EyeIcon from '../../Icons/EyeIcon'
|
||||
import EyeSlashIcon from '../../Icons/EyeSlashIcon'
|
||||
@ -20,7 +21,7 @@ const SecretDisplay = ({ value, reveal = false }) => {
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
|
||||
<Text code>{reveal && visible ? value : masked}</Text>
|
||||
{reveal && (
|
||||
<Tooltip title={visible ? 'Hide' : 'Show'} arrow={false}>
|
||||
<Tooltip title={visible ? 'Hide' : 'Show'}>
|
||||
<Button
|
||||
type='text'
|
||||
icon={visible ? <EyeSlashIcon /> : <EyeIcon />}
|
||||
|
||||
72
src/components/Dashboard/common/Tooltip.jsx
Normal file
72
src/components/Dashboard/common/Tooltip.jsx
Normal 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
|
||||
@ -1,5 +1,6 @@
|
||||
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 CopyButton from './CopyButton'
|
||||
import ElipsisText from './ElipsisText'
|
||||
@ -34,7 +35,7 @@ const UrlDisplay = ({ url, showCopy = true, showLink = false }) => {
|
||||
<ElipsisText style={{ marginRight: 8, minWidth: 0 }}>
|
||||
{url}
|
||||
</ElipsisText>
|
||||
<Tooltip title='Open URL' arrow={false}>
|
||||
<Tooltip title='Open URL'>
|
||||
<Button
|
||||
icon={<LinkIcon />}
|
||||
type='text'
|
||||
|
||||
@ -3,6 +3,7 @@ import PropTypes from 'prop-types'
|
||||
import { Button, Dropdown, Flex } from 'antd'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { ElectronContext } from '../context/ElectronContext'
|
||||
import { AuthContext } from '../context/AuthContext'
|
||||
import { useAppUpdateContext } from '../context/AppUpdateContext'
|
||||
import { getSidebarMenuSections } from '../../../database/Sidebars'
|
||||
import FarmControlLogoSmall from '../../Logos/FarmControlLogoSmall'
|
||||
@ -75,13 +76,14 @@ MenuButton.propTypes = {
|
||||
|
||||
const WindowAppMenu = () => {
|
||||
const navigate = useNavigate()
|
||||
const { userProfile } = useContext(AuthContext)
|
||||
const { handleWindowControl } = useContext(ElectronContext)
|
||||
const { checkForUpdates } = useAppUpdateContext()
|
||||
const includeDev = import.meta.env.DEV
|
||||
|
||||
const viewSections = useMemo(
|
||||
() => getSidebarMenuSections({ includeDev }),
|
||||
[includeDev]
|
||||
() => getSidebarMenuSections({ includeDev, userProfile }),
|
||||
[includeDev, userProfile]
|
||||
)
|
||||
|
||||
const appMenuItems = useMemo(
|
||||
|
||||
@ -120,6 +120,13 @@ const ActionsProvider = ({ children }) => {
|
||||
}
|
||||
|
||||
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 objectId =
|
||||
getObjectIdFromSearch(actionObjectType, location.search) ||
|
||||
|
||||
84
src/components/Dashboard/context/TooltipContext.jsx
Normal file
84
src/components/Dashboard/context/TooltipContext.jsx
Normal 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 }
|
||||
Loading…
x
Reference in New Issue
Block a user