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;
|
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;
|
||||||
|
|||||||
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 { 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>
|
||||||
|
|||||||
@ -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 }}
|
||||||
|
|||||||
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,
|
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 }}
|
||||||
|
|||||||
@ -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 (
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
@ -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 />}
|
||||||
|
|||||||
@ -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 ? (
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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 />}
|
||||||
|
|||||||
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 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'
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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) ||
|
||||||
|
|||||||
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