Enhance SpotlightTooltip Component and Update Styles
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good

- Refactored SpotlightTooltip component to improve action handling and data rendering.
- Introduced new utility functions for value checking and nested property access.
- Updated rendering logic to utilize ObjectCard for better display of spotlight data.
- Added CSS styles for spotlight tooltip to ensure a transparent background.
This commit is contained in:
Tom Butcher 2026-11-08 21:11:14 +00:00
parent 3544ac0d89
commit f0b0ad41a6
2 changed files with 123 additions and 73 deletions

View File

@ -484,6 +484,11 @@ code {
padding: 0 !important; padding: 0 !important;
} }
.spotlight-tooltip.ant-card,
.spotlight-tooltip .ant-card {
background: transparent !important;
}
.ant-popover:has(.spotlight-tooltip) { .ant-popover:has(.spotlight-tooltip) {
z-index: 1051; z-index: 1051;
} }

View File

@ -1,34 +1,102 @@
// SpotlightTooltip.js // SpotlightTooltip.js
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { Descriptions, Card, Flex, Typography, Skeleton, Spin } from 'antd' import { Button, Card, Flex, Typography } from 'antd'
import { LoadingOutlined } from '@ant-design/icons' import {
import { useEffect, useState, useContext, useCallback } from 'react' createElement,
useEffect,
useState,
useContext,
useCallback,
useMemo
} from 'react'
import { AuthContext } from '../context/AuthContext' import { AuthContext } from '../context/AuthContext'
import ObjectProperty from './ObjectProperty' import { useActions } from '../context/ActionsContext'
import InfoCircleIcon from '../../Icons/InfoCircleIcon' import InfoCircleIcon from '../../Icons/InfoCircleIcon'
import QuestionCircleIcon from '../../Icons/QuestionCircleIcon'
import { ApiServerContext } from '../context/ApiServerContext' import { ApiServerContext } from '../context/ApiServerContext'
import merge from 'lodash/merge' import merge from 'lodash/merge'
import { import { getModelByName } from '../../../database/ObjectModels'
getModelByName, import { hasActionPermission } from '../../../database/permissions'
getModelProperty import ObjectCard from './ObjectCard'
} from '../../../database/ObjectModels' import Tooltip from './Tooltip'
const { Text } = Typography const { Text } = Typography
const hasValue = (value) => value !== undefined && value !== null && value !== ''
const getNestedValue = (obj, path) => {
return path.split('.').reduce((current, key) => current?.[key], obj)
}
const SpotlightTooltip = ({ query, type }) => { const SpotlightTooltip = ({ query, type }) => {
const [spotlightData, setSpotlightData] = useState(null) const [spotlightData, setSpotlightData] = useState(null)
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [initialized, setInitialized] = useState(false) const [initialized, setInitialized] = useState(false)
const { token } = useContext(AuthContext) const { token, userProfile } = useContext(AuthContext)
const { callAction } = useActions()
const { fetchSpotlightData, subscribeToObjectUpdates, connected } = const { fetchSpotlightData, subscribeToObjectUpdates, connected } =
useContext(ApiServerContext) useContext(ApiServerContext)
// Get the model for this type
const model = getModelByName(type) const model = getModelByName(type)
const modelProperties = model.properties || [] const modelProperties = model.properties || []
const rowActions = useMemo(
() => model.actions?.filter((action) => action.row == true) || [],
[model]
)
const renderActions = useCallback(
(objectData, actionsDisabled = false) => {
return (
<Flex gap='small' align='center' justify='center'>
{rowActions.map((action, index) => {
const denied = !hasActionPermission(userProfile, model, action.name)
var disabled = denied
if (action.disabled) {
if (typeof action.disabled === 'function') {
disabled =
denied ||
action.disabled({
...objectData,
_user: userProfile
}) ||
actionsDisabled
} else {
disabled = denied || action.disabled || actionsDisabled
}
}
return (
<Tooltip key={index} title={action.label}>
<Button
icon={
action.icon ? (
createElement(action.icon)
) : (
<QuestionCircleIcon />
)
}
disabled={disabled || objectData?.isSkeleton}
type={'text'}
size={'small'}
danger={action?.danger || false}
onClick={() => {
if (denied) return
callAction(
action,
{ ...objectData, _user: userProfile },
type
)
}}
/>
</Tooltip>
)
})}
</Flex>
)
},
[callAction, model, rowActions, type, userProfile]
)
// Update event handler
const updateObjectEventHandler = useCallback((value) => { const updateObjectEventHandler = useCallback((value) => {
setSpotlightData((prev) => merge({}, prev, value)) setSpotlightData((prev) => merge({}, prev, value))
}, []) }, [])
@ -47,7 +115,6 @@ const SpotlightTooltip = ({ query, type }) => {
} }
}, [token, fetchSpotlight, initialized]) }, [token, fetchSpotlight, initialized])
// Subscribe to object updates when component mounts
useEffect(() => { useEffect(() => {
if (spotlightData?._id && type && connected && token) { if (spotlightData?._id && type && connected && token) {
const objectUpdatesUnsubscribe = subscribeToObjectUpdates( const objectUpdatesUnsubscribe = subscribeToObjectUpdates(
@ -69,6 +136,28 @@ const SpotlightTooltip = ({ query, type }) => {
updateObjectEventHandler updateObjectEventHandler
]) ])
const visibleColumns = useMemo(() => {
const columnNames = model.columns || []
const columns = { name: true }
if (!spotlightData) {
columnNames.slice(0, 4).forEach((colName) => {
columns[colName] = true
})
columnNames.forEach((colName) => {
if (columns[colName] == null) columns[colName] = false
})
return columns
}
for (const colName of columnNames) {
if (colName === 'name') continue
columns[colName] = hasValue(getNestedValue(spotlightData, colName))
}
return columns
}, [model.columns, spotlightData])
if (!spotlightData && !loading) { if (!spotlightData && !loading) {
return ( return (
<Card className='spotlight-tooltip'> <Card className='spotlight-tooltip'>
@ -87,68 +176,24 @@ const SpotlightTooltip = ({ query, type }) => {
) )
} }
// Helper to get nested property value
const getNestedValue = (obj, path) => {
return path.split('.').reduce((current, key) => current?.[key], obj)
}
return ( return (
<div className='spotlight-tooltip'> <div className='spotlight-tooltip' style={{ width: 360 }}>
<Spin indicator={<LoadingOutlined />} spinning={loading}> <ObjectCard
<Descriptions bordered column={1} size='small'> isSkeleton={!spotlightData && loading}
{loading ? ( model={model}
<> modelProperties={modelProperties}
<Descriptions.Item visibleColumns={visibleColumns}
label={ record={
<Skeleton.Input active size='small' style={{ width: 80 }} /> spotlightData || {
} _id: query,
> isSkeleton: true
<Skeleton.Input active size='small' style={{ width: 120 }} /> }
</Descriptions.Item> }
<Descriptions.Item isEditing={false}
label={ rowActions={rowActions}
<Skeleton.Input active size='small' style={{ width: 80 }} /> renderActions={renderActions}
} cardStyle='borderless'
> />
<Skeleton.Input active size='small' style={{ width: 120 }} />
</Descriptions.Item>
<Descriptions.Item
label={
<Skeleton.Input active size='small' style={{ width: 80 }} />
}
>
<Skeleton.Input active size='small' style={{ width: 120 }} />
</Descriptions.Item>
</>
) : (
modelProperties
.filter((prop) => {
const value = getNestedValue(spotlightData, prop.name)
return value !== undefined && value !== null && value !== ''
})
.map((prop) => {
const value = getNestedValue(spotlightData, prop.name)
return (
<Descriptions.Item key={prop.name} label={prop.label}>
<ObjectProperty
{...getModelProperty(type, prop.name)}
value={value}
objectData={spotlightData}
objectType={prop.objectType || type}
isEditing={false}
longId={false}
showSpotlight={false}
showLabel={false}
showName={false}
showId={false}
showQuantity={false}
/>
</Descriptions.Item>
)
})
)}
</Descriptions>
</Spin>
</div> </div>
) )
} }