Enhance Notification component with new features and improved structure
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good

- Introduced memoization for NotificationBody and NotificationDetails components to optimize rendering performance.
- Added new notification types and corresponding message handling in getNotificationMessage function for better user feedback.
- Updated Notification component to support inline icons and detailed views for notifications.
- Refactored notification icon handling to utilize model-specific icons where applicable.
- Adjusted NotificationCenter to display notifications with new props for enhanced visibility and interaction.
- Increased notification duration for better user experience.
This commit is contained in:
Tom Butcher 2026-07-25 01:36:10 +01:00
parent 03cd3db1c7
commit 41f844b49d
3 changed files with 345 additions and 121 deletions

View File

@ -1,12 +1,15 @@
import { useState } from 'react'
import { useState, memo, useMemo } from 'react'
import PropTypes from 'prop-types'
import { Typography, Button, Tag, Badge, Flex, Card, Divider } from 'antd'
import {
BellOutlined,
InfoCircleOutlined,
ExclamationCircleOutlined,
CheckCircleOutlined
} from '@ant-design/icons'
Typography,
Button,
Tag,
Badge,
Flex,
Card,
Divider,
Popover
} from 'antd'
import TimeDisplay from './TimeDisplay'
import EditIcon from '../../Icons/EditIcon'
import PropertyChanges from './PropertyChanges'
@ -15,9 +18,194 @@ import ObjectDisplay from './ObjectDisplay'
import BinIcon from '../../Icons/BinIcon'
import NoteItem from './NoteItem'
import PlusIcon from '../../Icons/PlusIcon'
import InfoCircleIcon from '../../Icons/InfoCircleIcon'
import ExclamationOctagonIcon from '../../Icons/ExclamationOctagonIcon'
import CheckCircleIcon from '../../Icons/CheckCircleIcon'
import BellIcon from '../../Icons/BellIcon'
import { getModelByName } from '../../../database/ObjectModels'
const { Text, Paragraph } = Typography
const getNotificationMessage = (type, metadata) => {
const objectType = metadata?.objectType ?? metadata?.note?.parentType
const model = objectType ? getModelByName(objectType) : null
const objectLabel = model?.label?.toLowerCase() ?? 'object'
switch (type) {
case 'editObject':
return `A ${objectLabel} was updated.`
case 'deleteObject':
return `A ${objectLabel} was deleted.`
case 'newNote':
return 'A new note was added.'
case 'info':
return 'You have a new notification.'
case 'error':
return 'An error occurred.'
case 'success':
return 'Action completed successfully.'
default:
return 'You have a new notification.'
}
}
const getParagraphProps = (largeSpacing) => ({
ellipsis: {
rows: 2,
expandable: true,
symbol: 'Show more'
},
style: {
margin: largeSpacing ? '10px 0 0 0' : 0,
lineHeight: 1.7,
marginBottom: 2
}
})
const NotificationBody = memo(function NotificationBody({
type,
message,
metadata,
largeSpacing
}) {
const paragraph = getParagraphProps(largeSpacing)
switch (type) {
case 'editObject':
if (metadata?.old || metadata?.new) {
return (
<PropertyChanges
type={metadata?.objectType ?? 'unknown'}
value={{
old: metadata.old,
new: metadata.new
}}
/>
)
}
return <Paragraph {...paragraph}>{message}</Paragraph>
case 'newNote':
return (
<NoteItem
note={metadata?.note}
showCard={false}
showCreatedAt={false}
showChildNotes={false}
showActions={false}
showInfo={false}
showBody={true}
showFooter={false}
largeSpacing={largeSpacing}
/>
)
case 'deleteObject':
return <Paragraph {...paragraph}>{message}</Paragraph>
case 'info':
case 'error':
case 'success':
return <Paragraph {...paragraph}>{message}</Paragraph>
default:
return <Paragraph {...paragraph}>{message}</Paragraph>
}
})
NotificationBody.propTypes = {
type: PropTypes.string,
message: PropTypes.string,
metadata: PropTypes.object,
largeSpacing: PropTypes.bool
}
const NotificationDetails = memo(function NotificationDetails({
type,
message,
metadata,
largeSpacing
}) {
const paragraph = getParagraphProps(largeSpacing)
const object = metadata?.object
const user = metadata?.user
const objectType = metadata?.objectType
const note = metadata?.note
switch (type) {
case 'editObject':
return (
<Flex align='center' gap='small'>
<Text {...paragraph} type='secondary'>
Object:
</Text>
<ObjectDisplay
object={object}
objectType={objectType}
showHyperlink={true}
showSpotlight={true}
/>
<Text {...paragraph} style={{ marginLeft: '5px' }} type='secondary'>
User:
</Text>
<ObjectDisplay
object={user}
objectType='user'
showHyperlink={true}
showSpotlight={true}
/>
</Flex>
)
case 'newNote':
return (
<Paragraph {...paragraph}>
<NoteItem
note={note}
showCard={false}
showCreatedAt={false}
showChildNotes={false}
showActions={false}
showInfo={false}
showBody={false}
largeSpacing={largeSpacing}
/>
</Paragraph>
)
case 'deleteObject':
return (
<Flex align='center' gap='small'>
<Text {...paragraph} type='secondary'>
Object:
</Text>
<ObjectDisplay
object={object}
objectType={objectType}
showHyperlink={true}
showSpotlight={true}
/>
<Text {...paragraph} style={{ marginLeft: '5px' }} type='secondary'>
User:
</Text>
<ObjectDisplay
object={user}
objectType='user'
showHyperlink={true}
showSpotlight={true}
/>
</Flex>
)
case 'info':
case 'error':
case 'success':
return <Paragraph {...paragraph}>{message}</Paragraph>
default:
return <Paragraph {...paragraph}>{message}</Paragraph>
}
})
NotificationDetails.propTypes = {
type: PropTypes.string,
message: PropTypes.string,
metadata: PropTypes.object,
largeSpacing: PropTypes.bool
}
const Notification = ({
notification,
onMarkAsRead,
@ -27,14 +215,16 @@ const Notification = ({
showExtraInfo = true,
inlineIcon = false,
largeSpacing = false,
showSince = true
showSince = true,
showDetails = true,
largeTitleSpacing = false
}) => {
const [deleting, setDeleting] = useState(false)
const getNotificationIcon = (type) => {
const getNotificationTagIcon = (type) => {
switch (type) {
case 'info':
return <InfoCircleOutlined />
return <InfoCircleIcon />
case 'editObject':
return <EditIcon />
case 'newNote':
@ -42,16 +232,29 @@ const Notification = ({
case 'deleteObject':
return <BinIcon />
case 'error':
return <ExclamationCircleOutlined />
return <ExclamationOctagonIcon />
case 'success':
return <CheckCircleOutlined />
return <CheckCircleIcon />
default:
return <BellOutlined />
return <BellIcon />
}
}
const getNotificationIcon = (type, metadata) => {
if (['editObject', 'newNote', 'deleteObject'].includes(type)) {
const objectType = metadata?.objectType ?? metadata?.note?.parentType
const model = objectType ? getModelByName(objectType) : null
if (model?.icon) {
const Icon = model.icon
return <Icon style={{ fontSize: 17 }} />
}
}
return getNotificationTagIcon(type)
}
const getNotificationTag = (type) => {
const icon = getNotificationIcon(type)
const icon = getNotificationTagIcon(type)
switch (type) {
case 'info':
return (
@ -98,71 +301,6 @@ const Notification = ({
}
}
const getMetadataDisplay = (metadata, type) => {
if (metadata.old && metadata.new && type === 'editObject') {
return (
<PropertyChanges
type={metadata?.objectType ?? 'unknown'}
value={{
old: metadata.old,
new: metadata.new
}}
/>
)
}
return null
}
const getNotificationMessage = (metadata, type) => {
const paragraph = {
ellipsis: {
rows: 2,
expandable: true,
symbol: 'Show more'
},
style: { margin: largeSpacing ? '10px 0 0 0' : 0 }
}
switch (type) {
case 'editObject': {
return (
<Paragraph {...paragraph}>
Object:
<ObjectDisplay
object={{ ...metadata?.object, _id: metadata?.object?._id }}
objectType={metadata.objectType}
showHyperlink={true}
showSpotlight={true}
/>
User:
<ObjectDisplay
object={metadata.user}
objectType='user'
showHyperlink={true}
showSpotlight={true}
/>
</Paragraph>
)
}
case 'newNote': {
return (
<Paragraph {...paragraph}>
<NoteItem
note={metadata.note}
showCard={false}
showCreatedAt={false}
showChildNotes={false}
showActions={false}
showInfo={false}
largeSpacing={largeSpacing}
/>
</Paragraph>
)
}
default:
return <Paragraph {...paragraph}>{notification.message}</Paragraph>
}
}
const handleMarkAsRead = () => {
if (onMarkAsRead && !notification.read) {
onMarkAsRead(notification._id)
@ -181,46 +319,114 @@ const Notification = ({
}
}
const message = useMemo(
() => getNotificationMessage(notification.type, notification.metadata),
[notification.type, notification.metadata]
)
const notificationDetails = useMemo(
() => (
<NotificationDetails
type={notification.type}
message={message}
metadata={notification.metadata}
largeSpacing={largeSpacing}
/>
),
[notification.type, message, notification.metadata, largeSpacing]
)
const notificationBody = useMemo(
() => (
<NotificationBody
type={notification.type}
message={message}
metadata={notification.metadata}
largeSpacing={largeSpacing}
/>
),
[notification.type, message, notification.metadata, largeSpacing]
)
const content = (
<Flex align='start' gap='small'>
<Flex vertical style={{ flex: 1 }} gap='small'>
<Flex align='start' gap='small' style={{ width: '100%' }}>
<Flex vertical style={{ flex: 1, width: '100%' }} gap='12px'>
<Flex vertical gap={largeTitleSpacing ? '10px' : '7px'}>
<Flex justify='space-between' align='center'>
<Flex align='center' gap='middle'>
{inlineIcon && getNotificationIcon(notification.type)}
<Badge dot={!notification.read} offset={[2, 4]}>
<Text strong={!notification.read}>{notification.title}</Text>
</Badge>
<Flex
align='center'
gap={largeTitleSpacing ? 'middle' : '8px'}
style={{ minWidth: 0 }}
>
{inlineIcon &&
getNotificationIcon(notification.type, notification.metadata)}
<Text
strong={!notification.read}
ellipsis
style={{ minWidth: 0 }}
>
{notification.title}
</Text>
</Flex>
<Flex align='center' gap='5px' style={{ marginLeft: 10 }}>
{showDetails && (
<Popover
arrow={false}
trigger={['hover', 'click']}
placement='bottomLeft'
content={
<div style={{ marginLeft: '6px' }}>
{notificationDetails}
</div>
}
>
<Button
type='text'
size='small'
onClick={handleMarkAsRead}
disabled={deleting}
icon={
<InfoCircleIcon
style={{ fontSize: 14, marginTop: 2.5, marginLeft: 0 }}
/>
}
></Button>
</Popover>
)}
{showDelete && (
<Badge
dot={!notification.read}
offset={[0, 3]}
style={{ minWidth: 0 }}
>
<Button
type='text'
size='small'
loading={deleting}
disabled={deleting}
style={{
width: 20,
height: 20,
position: 'relative',
left: 2
}}
icon={
<XMarkIcon
style={{ fontSize: 10, marginBottom: 5, marginLeft: 0 }}
style={{
fontSize: 12,
marginBottom: 4.5,
marginLeft: 0
}}
/>
}
onClick={handleDelete}
/>
</Badge>
)}
</Flex>
</Flex>
{getNotificationMessage(notification.metadata, notification.type)}
{notification.metadata &&
getMetadataDisplay(notification.metadata, notification.type)}
{notificationBody}
</Flex>
{showExtraInfo && (
<>
<Divider style={{ margin: largeSpacing ? '10px 0' : 0 }} />
<Flex ju stify='space-between' align='center'>
<Flex justify='space-between' align='center'>
{getNotificationTag(notification.type)}
<TimeDisplay
dateTime={notification.createdAt}
@ -238,7 +444,10 @@ const Notification = ({
size='small'
style={{
cursor: 'pointer',
transition: 'background-color 0.2s'
transition: 'background-color 0.2s',
backgroundColor: notification.metadata?.note?.noteType?.color
? notification.metadata.note.noteType.color + '26'
: undefined
}}
onClick={handleMarkAsRead}
styles={{ body: { padding: '10px 12px 12px 12px' } }}
@ -246,7 +455,7 @@ const Notification = ({
{content}
</Card>
) : (
<div style={{ marginTop: '-1px' }}>{content}</div>
<div style={{ marginTop: '-1px', width: '100%' }}>{content}</div>
)
}
@ -254,9 +463,16 @@ Notification.propTypes = {
notification: PropTypes.shape({
_id: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
message: PropTypes.string.isRequired,
type: PropTypes.oneOf(['info', 'warning', 'error', 'success', 'default'])
.isRequired,
type: PropTypes.oneOf([
'info',
'warning',
'error',
'success',
'default',
'editObject',
'deleteObject',
'newNote'
]).isRequired,
read: PropTypes.bool.isRequired,
createdAt: PropTypes.string.isRequired,
metadata: PropTypes.object
@ -268,7 +484,9 @@ Notification.propTypes = {
showExtraInfo: PropTypes.bool,
inlineIcon: PropTypes.bool,
largeSpacing: PropTypes.bool,
showSince: PropTypes.bool
showSince: PropTypes.bool,
showDetails: PropTypes.bool,
largeTitleSpacing: PropTypes.bool
}
export default Notification

View File

@ -56,7 +56,9 @@ const NotificationCenter = ({ visible }) => {
handleMarkAllAsRead()
} else if (key === 'clearAll') {
setClearNotificationsLoading(true)
deleteAllNotifications().finally(() => setClearNotificationsLoading(false))
deleteAllNotifications().finally(() =>
setClearNotificationsLoading(false)
)
}
}
}
@ -95,6 +97,8 @@ const NotificationCenter = ({ visible }) => {
<Flex vertical gap='small'>
{notifications.map((notification) => (
<Notification
inlineIcon={true}
showDetails={true}
key={notification._id}
notification={notification}
onMarkAsRead={handleMarkAsRead}

View File

@ -146,10 +146,12 @@ const NotificationProvider = ({ children }) => {
showDelete={false}
showExtraInfo={false}
inlineIcon={true}
showDetails={false}
largeTitleSpacing={true}
/>
),
icon: null,
duration: 3,
duration: 5,
key: notif._id
})
}