Improved notifications.
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
This commit is contained in:
parent
96f7713f4d
commit
9470adbb8a
@ -22,13 +22,22 @@ import MissingPlaceholder from './MissingPlaceholder'
|
||||
import NewNote from '../Management/Notes/NewNote'
|
||||
import { ApiServerContext } from '../context/ApiServerContext'
|
||||
import ExclamationOctagonIcon from '../../Icons/ExclamationOctagonIcon'
|
||||
import UserNotifierToggle from './UserNotifierToggle'
|
||||
import { AuthContext } from '../context/AuthContext'
|
||||
import { getModelByName } from '../../../database/ObjectModels'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
const { Text } = Typography
|
||||
|
||||
const NoteItem = ({ note }) => {
|
||||
const NoteItem = ({
|
||||
note,
|
||||
showCard = true,
|
||||
showCreatedAt = true,
|
||||
showChildNotes = true,
|
||||
showActions = true,
|
||||
showInfo = true,
|
||||
largeSpacing = false
|
||||
}) => {
|
||||
const [childNotes, setChildNotes] = useState([])
|
||||
const noteModel = getModelByName('note')
|
||||
const infoAction = noteModel.actions.filter(
|
||||
@ -151,15 +160,8 @@ const NoteItem = ({ note }) => {
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={note._id}
|
||||
size='small'
|
||||
style={{
|
||||
backgroundColor: note.noteType.color + '26',
|
||||
textAlign: 'left'
|
||||
}}
|
||||
>
|
||||
const noteItem = (
|
||||
<>
|
||||
<Flex vertical gap={'small'}>
|
||||
<Flex gap={'middle'} align='start'>
|
||||
<Space>
|
||||
@ -170,8 +172,10 @@ const NoteItem = ({ note }) => {
|
||||
<MarkdownDisplay content={note.content} />
|
||||
</div>
|
||||
</Flex>
|
||||
<Divider style={{ margin: 0 }} />
|
||||
<Divider style={{ margin: largeSpacing ? '10px 0' : 0 }} />
|
||||
<Flex wrap gap={'small'}>
|
||||
{showActions && (
|
||||
<>
|
||||
<Dropdown
|
||||
menu={{ items: dropdownItems }}
|
||||
trigger={['hover']}
|
||||
@ -179,6 +183,14 @@ const NoteItem = ({ note }) => {
|
||||
>
|
||||
<Button size='small'>Actions</Button>
|
||||
</Dropdown>
|
||||
<UserNotifierToggle
|
||||
type='note'
|
||||
size='small'
|
||||
objectData={note}
|
||||
disabled={false}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Space size={'small'} style={{ marginRight: 8 }}>
|
||||
<Text type='secondary'>Type:</Text>
|
||||
<Tag color={note.noteType.color} style={{ margin: 0 }}>
|
||||
@ -194,12 +206,15 @@ const NoteItem = ({ note }) => {
|
||||
showHyperlink={true}
|
||||
/>
|
||||
</Space>
|
||||
{showCreatedAt && (
|
||||
<Space size={'small'} style={{ marginRight: 8 }}>
|
||||
<Text type='secondary'>Created At:</Text>
|
||||
<TimeDisplay dateTime={note.createdAt} showSince={true} />
|
||||
</Space>
|
||||
)}
|
||||
<Flex style={{ flexGrow: 1 }} justify='end'>
|
||||
<Space size={'small'}>
|
||||
{showInfo && (
|
||||
<Button
|
||||
icon={<InfoIcon />}
|
||||
type='text'
|
||||
@ -208,9 +223,15 @@ const NoteItem = ({ note }) => {
|
||||
navigate(infoAction.url(note._id))
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{showChildNotes && (
|
||||
<Button
|
||||
icon={
|
||||
childNotesLoading ? <LoadingOutlined /> : <CaretLeftFilled />
|
||||
childNotesLoading ? (
|
||||
<LoadingOutlined />
|
||||
) : (
|
||||
<CaretLeftFilled />
|
||||
)
|
||||
}
|
||||
size='small'
|
||||
type='text'
|
||||
@ -222,6 +243,7 @@ const NoteItem = ({ note }) => {
|
||||
}}
|
||||
onClick={toggleExpand}
|
||||
/>
|
||||
)}
|
||||
</Space>
|
||||
</Flex>
|
||||
</Flex>
|
||||
@ -296,12 +318,33 @@ const NoteItem = ({ note }) => {
|
||||
>
|
||||
<Text>Are you sure you want to delete this note?</Text>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
|
||||
return showCard ? (
|
||||
<Card
|
||||
key={note._id}
|
||||
size='small'
|
||||
style={{
|
||||
backgroundColor: note.noteType.color + '26',
|
||||
textAlign: 'left'
|
||||
}}
|
||||
>
|
||||
{noteItem}
|
||||
</Card>
|
||||
) : (
|
||||
noteItem
|
||||
)
|
||||
}
|
||||
|
||||
NoteItem.propTypes = {
|
||||
note: PropTypes.object.isRequired
|
||||
note: PropTypes.object.isRequired,
|
||||
showCard: PropTypes.bool,
|
||||
showCreatedAt: PropTypes.bool,
|
||||
showChildNotes: PropTypes.bool,
|
||||
showActions: PropTypes.bool,
|
||||
largeSpacing: PropTypes.bool,
|
||||
showInfo: PropTypes.bool
|
||||
}
|
||||
|
||||
export default NoteItem
|
||||
|
||||
@ -13,6 +13,8 @@ import PropertyChanges from './PropertyChanges'
|
||||
import XMarkIcon from '../../Icons/XMarkIcon'
|
||||
import ObjectDisplay from './ObjectDisplay'
|
||||
import BinIcon from '../../Icons/BinIcon'
|
||||
import NoteItem from './NoteItem'
|
||||
import PlusIcon from '../../Icons/PlusIcon'
|
||||
|
||||
const { Text, Paragraph } = Typography
|
||||
|
||||
@ -35,6 +37,8 @@ const Notification = ({
|
||||
return <InfoCircleOutlined />
|
||||
case 'editObject':
|
||||
return <EditIcon />
|
||||
case 'newNote':
|
||||
return <PlusIcon />
|
||||
case 'deleteObject':
|
||||
return <BinIcon />
|
||||
case 'error':
|
||||
@ -67,6 +71,12 @@ const Notification = ({
|
||||
Delete
|
||||
</Tag>
|
||||
)
|
||||
case 'newNote':
|
||||
return (
|
||||
<Tag color='green' icon={icon} style={{ margin: 0 }}>
|
||||
Note
|
||||
</Tag>
|
||||
)
|
||||
case 'error':
|
||||
return (
|
||||
<Tag color='red' icon={icon} style={{ margin: 0 }}>
|
||||
@ -110,7 +120,7 @@ const Notification = ({
|
||||
expandable: true,
|
||||
symbol: 'Show more'
|
||||
},
|
||||
style: { margin: 0 }
|
||||
style: { margin: largeSpacing ? '10px 0 0 0' : 0 }
|
||||
}
|
||||
switch (type) {
|
||||
case 'editObject': {
|
||||
@ -118,7 +128,7 @@ const Notification = ({
|
||||
<Paragraph {...paragraph}>
|
||||
Object:
|
||||
<ObjectDisplay
|
||||
object={metadata.object}
|
||||
object={{ ...metadata?.object, _id: metadata?.object?._id }}
|
||||
objectType={metadata.objectType}
|
||||
showHyperlink={true}
|
||||
showSpotlight={true}
|
||||
@ -133,6 +143,21 @@ const Notification = ({
|
||||
</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>
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import { Typography, Flex, Badge, Tag, Popover } from 'antd'
|
||||
import { LoadingOutlined } from '@ant-design/icons'
|
||||
import { useState, useEffect, useContext, useCallback, useRef } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { getModelByName } from '../../../database/ObjectModels'
|
||||
@ -18,6 +19,7 @@ const ObjectDisplay = ({
|
||||
showSpotlight = true
|
||||
}) => {
|
||||
const [objectData, setObjectData] = useState(object)
|
||||
const [isHydrating, setIsHydrating] = useState(false)
|
||||
const { subscribeToObjectUpdates, connected, fetchSpotlightData } =
|
||||
useContext(ApiServerContext)
|
||||
const { token } = useContext(AuthContext)
|
||||
@ -44,7 +46,12 @@ const ObjectDisplay = ({
|
||||
const model = getModelByName(objectType)
|
||||
const spotlightQuery = `${model.prefix}:${obj._id}`
|
||||
const spotlightResult = await fetchSpotlightData(spotlightQuery)
|
||||
if (spotlightResult && typeof spotlightResult === 'object') {
|
||||
// Spotlight returns [] when not found; only use single-object results
|
||||
if (
|
||||
spotlightResult &&
|
||||
typeof spotlightResult === 'object' &&
|
||||
!Array.isArray(spotlightResult)
|
||||
) {
|
||||
return spotlightResult
|
||||
}
|
||||
} catch (err) {
|
||||
@ -57,7 +64,7 @@ const ObjectDisplay = ({
|
||||
|
||||
// Subscribe to object updates when component mounts
|
||||
useEffect(() => {
|
||||
if (object?._id && objectType && connected && token) {
|
||||
if (object?._id && objectType && connected && token != null) {
|
||||
const objectUpdatesUnsubscribe = subscribeToObjectUpdates(
|
||||
object._id,
|
||||
objectType,
|
||||
@ -79,22 +86,49 @@ const ObjectDisplay = ({
|
||||
|
||||
// Update local state when object prop changes
|
||||
useEffect(() => {
|
||||
if (idRef.current == object?._id) return
|
||||
idRef.current = object?._id
|
||||
if (token == null) return
|
||||
const isMinimal = isMinimalObject(object)
|
||||
// Only skip re-fetch when we have a minimal object and already hydrated this id
|
||||
if (isMinimal && idRef.current === object?._id) return
|
||||
let cancelled = false
|
||||
if (isMinimal) setIsHydrating(true)
|
||||
const hydrateObject = async () => {
|
||||
const fullObject = await fetchFullObjectIfNeeded(object)
|
||||
if (!cancelled) setObjectData(fullObject)
|
||||
if (!cancelled) {
|
||||
setObjectData((prev) => merge({}, prev, fullObject))
|
||||
if (isMinimal) idRef.current = object?._id
|
||||
setIsHydrating(false)
|
||||
}
|
||||
}
|
||||
hydrateObject()
|
||||
return () => {
|
||||
cancelled = true
|
||||
setIsHydrating(false)
|
||||
}
|
||||
}, [object, fetchFullObjectIfNeeded])
|
||||
}, [object, fetchFullObjectIfNeeded, isMinimalObject, token])
|
||||
if (!objectData) {
|
||||
return <Text type='secondary'>n/a</Text>
|
||||
}
|
||||
|
||||
if (isHydrating) {
|
||||
return (
|
||||
<Tag
|
||||
style={{
|
||||
margin: 0,
|
||||
border: 'none',
|
||||
minWidth: 0,
|
||||
maxWidth: '100%'
|
||||
}}
|
||||
className='object-display-tag'
|
||||
>
|
||||
<Flex gap='4px' align='center' style={{ minWidth: 0, height: '24px' }}>
|
||||
<LoadingOutlined spin />
|
||||
<Text type='secondary'>Loading...</Text>
|
||||
</Flex>
|
||||
</Tag>
|
||||
)
|
||||
}
|
||||
|
||||
const model = getModelByName(objectType)
|
||||
const Icon = model.icon
|
||||
const prefix = model.prefix
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import { useState, useEffect, useContext } from 'react'
|
||||
import { useState, useEffect, useContext, useRef, useCallback, useMemo, memo } from 'react'
|
||||
import { Button, message, Popover, Typography, Space, Flex } from 'antd'
|
||||
import { UserOutlined } from '@ant-design/icons'
|
||||
import BellIcon from '../../Icons/BellIcon'
|
||||
@ -12,9 +12,10 @@ import InfoCircleIcon from '../../Icons/InfoCircleIcon'
|
||||
|
||||
const { Text } = Typography
|
||||
|
||||
const UserNotifierToggle = ({
|
||||
const UserNotifierToggle = memo(({
|
||||
type,
|
||||
objectData,
|
||||
size = 'middle',
|
||||
disabled = false,
|
||||
...buttonProps
|
||||
}) => {
|
||||
@ -24,7 +25,7 @@ const UserNotifierToggle = ({
|
||||
fetchUserNotifiersForObject,
|
||||
fetchAllUserNotifiersForObject
|
||||
} = useContext(ApiServerContext)
|
||||
const { userProfile } = useContext(AuthContext)
|
||||
const { userProfile, token, authInitialized } = useContext(AuthContext)
|
||||
const [isNotifying, setIsNotifying] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [initialLoad, setInitialLoad] = useState(true)
|
||||
@ -34,14 +35,24 @@ const UserNotifierToggle = ({
|
||||
const [emailTogglingId, setEmailTogglingId] = useState(null)
|
||||
|
||||
const objectId = objectData?._id
|
||||
const authReady = Boolean(token && authInitialized)
|
||||
|
||||
const apiRef = useRef({
|
||||
fetchUserNotifiersForObject,
|
||||
fetchAllUserNotifiersForObject
|
||||
})
|
||||
apiRef.current = {
|
||||
fetchUserNotifiersForObject,
|
||||
fetchAllUserNotifiersForObject
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const loadNotifierState = async () => {
|
||||
if (!objectId || !type) return
|
||||
if (!authReady || !objectId || !type) return
|
||||
|
||||
const loadNotifierState = async () => {
|
||||
setInitialLoad(true)
|
||||
try {
|
||||
const { data } = await fetchUserNotifiersForObject(objectId, type)
|
||||
const { data } = await apiRef.current.fetchUserNotifiersForObject(objectId, type)
|
||||
setIsNotifying(data?.length > 0)
|
||||
} catch (error) {
|
||||
console.error('Error fetching user notifier state:', error)
|
||||
@ -51,15 +62,15 @@ const UserNotifierToggle = ({
|
||||
}
|
||||
|
||||
loadNotifierState()
|
||||
}, [objectId, type, fetchUserNotifiersForObject])
|
||||
}, [authReady, objectId, type])
|
||||
|
||||
useEffect(() => {
|
||||
const loadAllNotifiers = async () => {
|
||||
if (!objectId || !type || !popoverOpen) return
|
||||
if (!authReady || !objectId || !type || !popoverOpen) return
|
||||
|
||||
const loadAllNotifiers = async () => {
|
||||
setPopoverLoading(true)
|
||||
try {
|
||||
const { data } = await fetchAllUserNotifiersForObject(objectId, type)
|
||||
const { data } = await apiRef.current.fetchAllUserNotifiersForObject(objectId, type)
|
||||
setAllNotifiers(data || [])
|
||||
} catch (error) {
|
||||
console.error('Error fetching all user notifiers:', error)
|
||||
@ -70,10 +81,10 @@ const UserNotifierToggle = ({
|
||||
}
|
||||
|
||||
loadAllNotifiers()
|
||||
}, [objectId, type, popoverOpen, fetchAllUserNotifiersForObject])
|
||||
}, [authReady, objectId, type, popoverOpen])
|
||||
|
||||
const handleClick = async () => {
|
||||
if (!objectId || !type || loading) return
|
||||
const handleClick = useCallback(async () => {
|
||||
if (!authReady || !objectId || !type || loading) return
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
@ -94,9 +105,9 @@ const UserNotifierToggle = ({
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
}, [authReady, objectId, type, loading, popoverOpen, toggleUserNotifier, fetchAllUserNotifiersForObject])
|
||||
|
||||
const getUserDisplayName = (user) => {
|
||||
const getUserDisplayName = useCallback((user) => {
|
||||
if (!user) return 'Unknown'
|
||||
return (
|
||||
user.name ||
|
||||
@ -105,11 +116,11 @@ const UserNotifierToggle = ({
|
||||
user.email ||
|
||||
'Unknown'
|
||||
)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const isCurrentUser = (user) => user?._id === userProfile?._id
|
||||
const isCurrentUser = useCallback((user) => user?._id === userProfile?._id, [userProfile?._id])
|
||||
|
||||
const handleEmailToggle = async (item) => {
|
||||
const handleEmailToggle = useCallback(async (item) => {
|
||||
if (!isCurrentUser(item.user) || emailTogglingId) return
|
||||
setEmailTogglingId(item._id)
|
||||
const newEmail = !item.email
|
||||
@ -133,9 +144,10 @@ const UserNotifierToggle = ({
|
||||
} finally {
|
||||
setEmailTogglingId(null)
|
||||
}
|
||||
}
|
||||
}, [isCurrentUser, emailTogglingId, editUserNotifier])
|
||||
|
||||
const popoverContent = (
|
||||
const popoverContent = useMemo(
|
||||
() => (
|
||||
<Flex
|
||||
vertical
|
||||
justify='center'
|
||||
@ -198,6 +210,8 @@ const UserNotifierToggle = ({
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
),
|
||||
[popoverLoading, allNotifiers, emailTogglingId, isCurrentUser, getUserDisplayName, handleEmailToggle]
|
||||
)
|
||||
|
||||
return (
|
||||
@ -220,18 +234,23 @@ const UserNotifierToggle = ({
|
||||
}}
|
||||
/>
|
||||
}
|
||||
disabled={disabled || loading || initialLoad}
|
||||
loading={loading || !objectId || !type}
|
||||
disabled={disabled || loading || initialLoad || !authReady}
|
||||
loading={loading || !authReady || !objectId || !type}
|
||||
onClick={handleClick}
|
||||
size={size}
|
||||
style={{ minWidth: size === 'small' ? 24 : undefined }}
|
||||
/>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
UserNotifierToggle.displayName = 'UserNotifierToggle'
|
||||
|
||||
UserNotifierToggle.propTypes = {
|
||||
type: PropTypes.string.isRequired,
|
||||
objectData: PropTypes.object.isRequired,
|
||||
disabled: PropTypes.bool
|
||||
disabled: PropTypes.bool,
|
||||
size: PropTypes.string
|
||||
}
|
||||
|
||||
export default UserNotifierToggle
|
||||
|
||||
@ -20,6 +20,11 @@ import loglevel from 'loglevel'
|
||||
const logger = loglevel.getLogger('ApiServerContext')
|
||||
logger.setLevel(config.logLevel)
|
||||
|
||||
const SPOTLIGHT_CACHE_TTL_MS = 10_000
|
||||
|
||||
const spotlightCache = new Map()
|
||||
const runningSpotlightFetches = new Map()
|
||||
|
||||
const ApiServerContext = createContext()
|
||||
|
||||
const ApiServerProvider = ({ children }) => {
|
||||
@ -974,8 +979,20 @@ const ApiServerProvider = ({ children }) => {
|
||||
}
|
||||
|
||||
const fetchSpotlightData = async (query) => {
|
||||
logger.debug('Fetching spotlight data with query:', query)
|
||||
const cached = spotlightCache.get(query)
|
||||
if (cached && Date.now() - cached.timestamp < SPOTLIGHT_CACHE_TTL_MS) {
|
||||
logger.debug('Returning cached spotlight data for query:', query)
|
||||
return cached.data
|
||||
}
|
||||
|
||||
const existing = runningSpotlightFetches.get(query)
|
||||
if (existing) {
|
||||
return existing
|
||||
}
|
||||
|
||||
const fetchPromise = (async () => {
|
||||
try {
|
||||
logger.debug('Fetching spotlight data with query:', query)
|
||||
const response = await axios.get(
|
||||
`${config.backendUrl}/spotlight/${query}`,
|
||||
{
|
||||
@ -985,13 +1002,20 @@ const ApiServerProvider = ({ children }) => {
|
||||
}
|
||||
}
|
||||
)
|
||||
spotlightCache.set(query, { data: response.data, timestamp: Date.now() })
|
||||
return response.data
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
showError(err, () => {
|
||||
fetchSpotlightData(query)
|
||||
})
|
||||
} finally {
|
||||
runningSpotlightFetches.delete(query)
|
||||
}
|
||||
})()
|
||||
|
||||
runningSpotlightFetches.set(query, fetchPromise)
|
||||
return fetchPromise
|
||||
}
|
||||
|
||||
const getModelStats = async (objectType) => {
|
||||
|
||||
@ -297,6 +297,7 @@ const AuthProvider = ({ children }) => {
|
||||
setToken(nextToken)
|
||||
setExpiresAt(nextExpiresAt)
|
||||
setUserProfile(nextUser)
|
||||
setAuthenticated(true)
|
||||
|
||||
// Persist session (cookies on web, keytar on electron)
|
||||
const persisted = await persistSession({
|
||||
@ -619,6 +620,7 @@ const AuthProvider = ({ children }) => {
|
||||
<AuthContext.Provider
|
||||
value={{
|
||||
authenticated,
|
||||
authInitialized: retreivedTokenFromCookies,
|
||||
setUnauthenticated,
|
||||
loginWithSSO,
|
||||
getLoginToken,
|
||||
|
||||
@ -5,6 +5,7 @@ import {
|
||||
useCallback,
|
||||
useEffect
|
||||
} from 'react'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import { notification, Drawer } from 'antd'
|
||||
import PropTypes from 'prop-types'
|
||||
import { AuthContext } from './AuthContext'
|
||||
@ -16,6 +17,7 @@ const NotificationContext = createContext()
|
||||
|
||||
const NotificationProvider = ({ children }) => {
|
||||
const [api, contextHolder] = notification.useNotification()
|
||||
const location = useLocation()
|
||||
const { authenticated } = useContext(AuthContext)
|
||||
const {
|
||||
showError,
|
||||
@ -119,6 +121,10 @@ const NotificationProvider = ({ children }) => {
|
||||
}
|
||||
}, [authenticated, fetchNotifications])
|
||||
|
||||
useEffect(() => {
|
||||
setNotificationCenterVisible(false)
|
||||
}, [location.pathname])
|
||||
|
||||
useEffect(() => {
|
||||
if (!authenticated || !registerNotificationListener) return
|
||||
const handleNotification = (notif) => {
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { useSearchParams } from 'react-router-dom'
|
||||
|
||||
import './EmailNotificationTemplate.css'
|
||||
import { Button, Card, ConfigProvider, Flex, theme, Typography } from 'antd'
|
||||
import FarmControlLogo from '../Logos/FarmControlLogo'
|
||||
import Notification from '../Dashboard/common/Notification'
|
||||
import { useThemeContext } from '../Dashboard/context/ThemeContext'
|
||||
import { getModelByName } from '../../database/ObjectModels'
|
||||
|
||||
const { Text } = Typography
|
||||
|
||||
@ -44,16 +44,33 @@ const EmailNotificationTemplate = () => {
|
||||
|
||||
const getNotifictionActions = () => {
|
||||
switch (type) {
|
||||
case 'editObject':
|
||||
case 'editObject': {
|
||||
const model = getModelByName(metadata?.objectType)
|
||||
const infoAction = model.actions?.find(
|
||||
(action) => action.name === 'info'
|
||||
)
|
||||
const url = infoAction ? infoAction.url(metadata?.object?._id) : '#'
|
||||
return (
|
||||
<Button
|
||||
href={`${origin}/dashboard/${metadata.objectType}/${metadata.id}`}
|
||||
type='primary'
|
||||
>
|
||||
<Button href={`${origin}${url}`} type='primary'>
|
||||
View in Dashboard
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
case 'newNote': {
|
||||
console.log('newNote metadata', JSON.stringify(metadata, null, 2))
|
||||
const model = getModelByName(metadata?.note?.parentType)
|
||||
const infoAction =
|
||||
model.actions?.find((action) => action.name === 'info') ?? null
|
||||
const url = infoAction
|
||||
? infoAction.url(metadata?.note?.parent?._id)
|
||||
: null
|
||||
return url ? (
|
||||
<Button href={`${origin}${url}`} type='primary'>
|
||||
View in Dashboard
|
||||
</Button>
|
||||
) : null
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@ -92,7 +109,11 @@ const EmailNotificationTemplate = () => {
|
||||
{getNotifictionActions()}
|
||||
</Flex>
|
||||
{email && (
|
||||
<Flex justify='center' className='email-notification-card-footer'>
|
||||
<Flex
|
||||
align='center'
|
||||
className='email-notification-card-footer'
|
||||
vertical
|
||||
>
|
||||
<Text
|
||||
type='secondary'
|
||||
style={{
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user