Enhance Notification component with new features and improved structure
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
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:
parent
03cd3db1c7
commit
41f844b49d
@ -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 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='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={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}
|
||||
icon={
|
||||
<XMarkIcon
|
||||
style={{
|
||||
fontSize: 12,
|
||||
marginBottom: 4.5,
|
||||
marginLeft: 0
|
||||
}}
|
||||
/>
|
||||
}
|
||||
onClick={handleDelete}
|
||||
/>
|
||||
</Badge>
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
{showDelete && (
|
||||
<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 }}
|
||||
/>
|
||||
}
|
||||
onClick={handleDelete}
|
||||
/>
|
||||
)}
|
||||
|
||||
{notificationBody}
|
||||
</Flex>
|
||||
|
||||
{getNotificationMessage(notification.metadata, notification.type)}
|
||||
|
||||
{notification.metadata &&
|
||||
getMetadataDisplay(notification.metadata, notification.type)}
|
||||
{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
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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
|
||||
})
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user