Compare commits

..

5 Commits

Author SHA1 Message Date
41f844b49d 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.
2026-07-25 01:36:10 +01:00
03cd3db1c7 Enhance NoteItem component with new props for body and footer visibility
- Added showBody and showFooter props to the NoteItem component to control the rendering of the note's content and footer elements.
- Refactored the component structure to conditionally display the body and footer based on the new props, improving flexibility in note presentation.
2026-07-25 01:35:41 +01:00
6471434d81 Update CountryDisplay component to improve layout consistency
- Added minWidth style to Flex and inner div elements to ensure proper alignment and prevent overflow issues in the CountryDisplay component.
2026-07-25 01:35:35 +01:00
eec1f14512 Add margin reset for preformatted text in minimal code block editor
- Introduced a margin reset for preformatted text within the minimal code block editor to ensure consistent styling and alignment.
2026-07-25 01:35:28 +01:00
2eda23fd06 Enhance CodeBlockEditor styling for minimal mode
- Added a class name 'minimal-code-block-editor' to the div in minimal mode for improved styling and potential CSS targeting.
2026-07-25 01:35:24 +01:00
7 changed files with 453 additions and 207 deletions

View File

@ -782,3 +782,7 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton {
.ant-descriptions .ant-descriptions-item-label::after { .ant-descriptions .ant-descriptions-item-label::after {
top: 0; top: 0;
} }
.minimal-code-block-editor .ant-typography pre {
margin: 0;
}

View File

@ -140,7 +140,10 @@ export default function CodeBlockEditor({
return ( return (
<> <>
{minimal ? ( {minimal ? (
<div style={{ maxWidth: '300px' }}> <div
style={{ maxWidth: '300px' }}
className='minimal-code-block-editor'
>
<Link <Link
onClick={() => { onClick={() => {
setCodeMirrorOpen(true) setCodeMirrorOpen(true)

View File

@ -13,14 +13,19 @@ const CountryDisplay = ({ countryCode }) => {
} }
return ( return (
<Flex gap={'small'} align='center' className='country-display'> <Flex
gap={'small'}
align='center'
className='country-display'
style={{ minWidth: 0 }}
>
<Flag <Flag
code={country.code} code={country.code}
size='middle' size='middle'
hasBorder={true} hasBorder={true}
borderRadius={5} borderRadius={5}
/> />
<div> <div style={{ minWidth: 0 }}>
<Text ellipsis>{country.name}</Text> <Text ellipsis>{country.name}</Text>
</div> </div>
</Flex> </Flex>

View File

@ -36,6 +36,8 @@ const NoteItem = ({
showChildNotes = true, showChildNotes = true,
showActions = true, showActions = true,
showInfo = true, showInfo = true,
showBody = true,
showFooter = true,
largeSpacing = false largeSpacing = false
}) => { }) => {
const [childNotes, setChildNotes] = useState([]) const [childNotes, setChildNotes] = useState([])
@ -162,16 +164,21 @@ const NoteItem = ({
const noteItem = ( const noteItem = (
<> <>
<Flex vertical gap={'small'}> <Flex vertical gap={'small'}>
{showBody && (
<Flex gap={'middle'} align='start' style={{ marginBottom: '4px' }}> <Flex gap={'middle'} align='start' style={{ marginBottom: '4px' }}>
<Space> <Space>
<PersonIcon /> <PersonIcon />
<Text style={{ whiteSpace: 'nowrap' }}>{note.user.name}:</Text> <Text style={{ whiteSpace: 'nowrap' }}>{note.user.name}:</Text>
</Space> </Space>
<div style={{ marginTop: '1px' }}> <div style={{ marginTop: '4px' }}>
<MarkdownDisplay content={note.content} /> <MarkdownDisplay content={note.content} />
</div> </div>
</Flex> </Flex>
)}
{showFooter && showBody && (
<Divider style={{ margin: largeSpacing ? '10px 0' : 0 }} /> <Divider style={{ margin: largeSpacing ? '10px 0' : 0 }} />
)}
{showFooter && (
<Flex wrap gap={'small'}> <Flex wrap gap={'small'}>
{showActions && ( {showActions && (
<> <>
@ -246,6 +253,7 @@ const NoteItem = ({
</Space> </Space>
</Flex> </Flex>
</Flex> </Flex>
)}
{isExpanded && ( {isExpanded && (
<Flex vertical gap={'small'} style={{ flexGrow: 1 }}> <Flex vertical gap={'small'} style={{ flexGrow: 1 }}>
{childNotes.length > 0 ? ( {childNotes.length > 0 ? (
@ -342,7 +350,9 @@ NoteItem.propTypes = {
showChildNotes: PropTypes.bool, showChildNotes: PropTypes.bool,
showActions: PropTypes.bool, showActions: PropTypes.bool,
largeSpacing: PropTypes.bool, largeSpacing: PropTypes.bool,
showInfo: PropTypes.bool showInfo: PropTypes.bool,
showBody: PropTypes.bool,
showFooter: PropTypes.bool
} }
export default NoteItem export default NoteItem

View File

@ -1,12 +1,15 @@
import { useState } from 'react' import { useState, memo, useMemo } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { Typography, Button, Tag, Badge, Flex, Card, Divider } from 'antd'
import { import {
BellOutlined, Typography,
InfoCircleOutlined, Button,
ExclamationCircleOutlined, Tag,
CheckCircleOutlined Badge,
} from '@ant-design/icons' Flex,
Card,
Divider,
Popover
} from 'antd'
import TimeDisplay from './TimeDisplay' import TimeDisplay from './TimeDisplay'
import EditIcon from '../../Icons/EditIcon' import EditIcon from '../../Icons/EditIcon'
import PropertyChanges from './PropertyChanges' import PropertyChanges from './PropertyChanges'
@ -15,9 +18,194 @@ import ObjectDisplay from './ObjectDisplay'
import BinIcon from '../../Icons/BinIcon' import BinIcon from '../../Icons/BinIcon'
import NoteItem from './NoteItem' import NoteItem from './NoteItem'
import PlusIcon from '../../Icons/PlusIcon' 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 { 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 = ({ const Notification = ({
notification, notification,
onMarkAsRead, onMarkAsRead,
@ -27,14 +215,16 @@ const Notification = ({
showExtraInfo = true, showExtraInfo = true,
inlineIcon = false, inlineIcon = false,
largeSpacing = false, largeSpacing = false,
showSince = true showSince = true,
showDetails = true,
largeTitleSpacing = false
}) => { }) => {
const [deleting, setDeleting] = useState(false) const [deleting, setDeleting] = useState(false)
const getNotificationIcon = (type) => { const getNotificationTagIcon = (type) => {
switch (type) { switch (type) {
case 'info': case 'info':
return <InfoCircleOutlined /> return <InfoCircleIcon />
case 'editObject': case 'editObject':
return <EditIcon /> return <EditIcon />
case 'newNote': case 'newNote':
@ -42,16 +232,29 @@ const Notification = ({
case 'deleteObject': case 'deleteObject':
return <BinIcon /> return <BinIcon />
case 'error': case 'error':
return <ExclamationCircleOutlined /> return <ExclamationOctagonIcon />
case 'success': case 'success':
return <CheckCircleOutlined /> return <CheckCircleIcon />
default: 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 getNotificationTag = (type) => {
const icon = getNotificationIcon(type) const icon = getNotificationTagIcon(type)
switch (type) { switch (type) {
case 'info': case 'info':
return ( 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 = () => { const handleMarkAsRead = () => {
if (onMarkAsRead && !notification.read) { if (onMarkAsRead && !notification.read) {
onMarkAsRead(notification._id) onMarkAsRead(notification._id)
@ -181,42 +319,110 @@ 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 = ( const content = (
<Flex align='start' gap='small'> <Flex align='start' gap='small' style={{ width: '100%' }}>
<Flex vertical style={{ flex: 1 }} gap='small'> <Flex vertical style={{ flex: 1, width: '100%' }} gap='12px'>
<Flex vertical gap={largeTitleSpacing ? '10px' : '7px'}>
<Flex justify='space-between' align='center'> <Flex justify='space-between' align='center'>
<Flex align='center' gap='middle'> <Flex
{inlineIcon && getNotificationIcon(notification.type)} align='center'
<Badge dot={!notification.read} offset={[2, 4]}> gap={largeTitleSpacing ? 'middle' : '8px'}
<Text strong={!notification.read}>{notification.title}</Text> style={{ minWidth: 0 }}
</Badge> >
{inlineIcon &&
getNotificationIcon(notification.type, notification.metadata)}
<Text
strong={!notification.read}
ellipsis
style={{ minWidth: 0 }}
>
{notification.title}
</Text>
</Flex> </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 && ( {showDelete && (
<Badge
dot={!notification.read}
offset={[0, 3]}
style={{ minWidth: 0 }}
>
<Button <Button
type='text' type='text'
size='small' size='small'
loading={deleting} loading={deleting}
disabled={deleting} disabled={deleting}
style={{
width: 20,
height: 20,
position: 'relative',
left: 2
}}
icon={ icon={
<XMarkIcon <XMarkIcon
style={{ fontSize: 10, marginBottom: 5, marginLeft: 0 }} style={{
fontSize: 12,
marginBottom: 4.5,
marginLeft: 0
}}
/> />
} }
onClick={handleDelete} onClick={handleDelete}
/> />
</Badge>
)} )}
</Flex> </Flex>
</Flex>
{getNotificationMessage(notification.metadata, notification.type)} {notificationBody}
</Flex>
{notification.metadata &&
getMetadataDisplay(notification.metadata, notification.type)}
{showExtraInfo && ( {showExtraInfo && (
<> <>
<Divider style={{ margin: largeSpacing ? '10px 0' : 0 }} /> <Divider style={{ margin: largeSpacing ? '10px 0' : 0 }} />
@ -238,7 +444,10 @@ const Notification = ({
size='small' size='small'
style={{ style={{
cursor: 'pointer', 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} onClick={handleMarkAsRead}
styles={{ body: { padding: '10px 12px 12px 12px' } }} styles={{ body: { padding: '10px 12px 12px 12px' } }}
@ -246,7 +455,7 @@ const Notification = ({
{content} {content}
</Card> </Card>
) : ( ) : (
<div style={{ marginTop: '-1px' }}>{content}</div> <div style={{ marginTop: '-1px', width: '100%' }}>{content}</div>
) )
} }
@ -254,9 +463,16 @@ Notification.propTypes = {
notification: PropTypes.shape({ notification: PropTypes.shape({
_id: PropTypes.string.isRequired, _id: PropTypes.string.isRequired,
title: PropTypes.string.isRequired, title: PropTypes.string.isRequired,
message: PropTypes.string.isRequired, type: PropTypes.oneOf([
type: PropTypes.oneOf(['info', 'warning', 'error', 'success', 'default']) 'info',
.isRequired, 'warning',
'error',
'success',
'default',
'editObject',
'deleteObject',
'newNote'
]).isRequired,
read: PropTypes.bool.isRequired, read: PropTypes.bool.isRequired,
createdAt: PropTypes.string.isRequired, createdAt: PropTypes.string.isRequired,
metadata: PropTypes.object metadata: PropTypes.object
@ -268,7 +484,9 @@ Notification.propTypes = {
showExtraInfo: PropTypes.bool, showExtraInfo: PropTypes.bool,
inlineIcon: PropTypes.bool, inlineIcon: PropTypes.bool,
largeSpacing: PropTypes.bool, largeSpacing: PropTypes.bool,
showSince: PropTypes.bool showSince: PropTypes.bool,
showDetails: PropTypes.bool,
largeTitleSpacing: PropTypes.bool
} }
export default Notification export default Notification

View File

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

View File

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