- Updated DocumentPrintButton to include subscription handling for document templates, improving real-time updates and user experience. - Refactored UserNotifierToggle to manage user notifier states with enhanced subscription logic, ensuring accurate updates and improved performance. - Introduced memoization and callback optimizations to streamline data fetching and state management in both components.
448 lines
13 KiB
JavaScript
448 lines
13 KiB
JavaScript
import PropTypes from 'prop-types'
|
|
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'
|
|
import MailCheckIcon from '../../Icons/MailCheckIcon'
|
|
import MailIcon from '../../Icons/MailIcon'
|
|
import { ApiServerContext } from '../context/ApiServerContext'
|
|
import { AuthContext } from '../context/AuthContext'
|
|
import { LoadingOutlined } from '@ant-design/icons'
|
|
import InfoCircleIcon from '../../Icons/InfoCircleIcon'
|
|
|
|
const { Text } = Typography
|
|
|
|
const UserNotifierToggle = memo(({
|
|
type,
|
|
objectData,
|
|
size = 'middle',
|
|
disabled = false,
|
|
...buttonProps
|
|
}) => {
|
|
const {
|
|
toggleUserNotifier,
|
|
editUserNotifier,
|
|
fetchUserNotifiersForObject,
|
|
fetchAllUserNotifiersForObject,
|
|
connected,
|
|
subscribeToObjectUpdates,
|
|
subscribeToObjectTypeUpdates
|
|
} = useContext(ApiServerContext)
|
|
const { userProfile, token, authInitialized } = useContext(AuthContext)
|
|
const [currentUserNotifier, setCurrentUserNotifier] = useState(null)
|
|
const [loading, setLoading] = useState(false)
|
|
const [initialLoad, setInitialLoad] = useState(true)
|
|
const [allNotifiers, setAllNotifiers] = useState([])
|
|
const [popoverOpen, setPopoverOpen] = useState(false)
|
|
const [popoverLoading, setPopoverLoading] = useState(false)
|
|
const [emailTogglingId, setEmailTogglingId] = useState(null)
|
|
|
|
const subscribedIdsRef = useRef([])
|
|
const unsubscribesRef = useRef([])
|
|
const subscribeToObjectTypeUpdatesRef = useRef(null)
|
|
const updateEventHandlerRef = useRef()
|
|
const popoverOpenRef = useRef(popoverOpen)
|
|
popoverOpenRef.current = popoverOpen
|
|
|
|
const objectId = objectData?._id
|
|
const authReady = Boolean(token && authInitialized)
|
|
const isNotifying = Boolean(currentUserNotifier)
|
|
|
|
const subscriptionFilter = useMemo(
|
|
() => ({
|
|
object: objectId,
|
|
objectType: type
|
|
}),
|
|
[objectId, type]
|
|
)
|
|
|
|
const notifierMatchesFilter = useCallback(
|
|
(notifier) => {
|
|
const objectRef = notifier?.object?._id ?? notifier?.object
|
|
return objectRef === objectId && notifier?.objectType === type
|
|
},
|
|
[objectId, type]
|
|
)
|
|
|
|
const apiRef = useRef({
|
|
fetchUserNotifiersForObject,
|
|
fetchAllUserNotifiersForObject
|
|
})
|
|
apiRef.current = {
|
|
fetchUserNotifiersForObject,
|
|
fetchAllUserNotifiersForObject
|
|
}
|
|
|
|
const loadNotifierState = useCallback(
|
|
async (silent = false) => {
|
|
if (!authReady || !objectId || !type) return
|
|
|
|
if (!silent) {
|
|
setInitialLoad(true)
|
|
}
|
|
try {
|
|
const { data } = await apiRef.current.fetchUserNotifiersForObject(
|
|
objectId,
|
|
type
|
|
)
|
|
setCurrentUserNotifier(data?.[0] ?? null)
|
|
} catch (error) {
|
|
console.error('Error fetching user notifier state:', error)
|
|
} finally {
|
|
if (!silent) {
|
|
setInitialLoad(false)
|
|
}
|
|
}
|
|
},
|
|
[authReady, objectId, type]
|
|
)
|
|
|
|
const loadAllNotifiers = useCallback(
|
|
async (silent = false) => {
|
|
if (!authReady || !objectId || !type) return
|
|
|
|
if (!silent) {
|
|
setPopoverLoading(true)
|
|
}
|
|
try {
|
|
const { data } = await apiRef.current.fetchAllUserNotifiersForObject(
|
|
objectId,
|
|
type
|
|
)
|
|
setAllNotifiers(data || [])
|
|
} catch (error) {
|
|
console.error('Error fetching all user notifiers:', error)
|
|
setAllNotifiers([])
|
|
} finally {
|
|
if (!silent) {
|
|
setPopoverLoading(false)
|
|
}
|
|
}
|
|
},
|
|
[authReady, objectId, type]
|
|
)
|
|
|
|
const loadNotifierStateRef = useRef(loadNotifierState)
|
|
loadNotifierStateRef.current = loadNotifierState
|
|
|
|
const loadAllNotifiersRef = useRef(loadAllNotifiers)
|
|
loadAllNotifiersRef.current = loadAllNotifiers
|
|
|
|
useEffect(() => {
|
|
loadNotifierState()
|
|
}, [loadNotifierState])
|
|
|
|
useEffect(() => {
|
|
if (!popoverOpen) return
|
|
loadAllNotifiers()
|
|
}, [popoverOpen, loadAllNotifiers])
|
|
|
|
const updateEventHandler = useCallback(
|
|
(id, updatedData) => {
|
|
setAllNotifiers((prev) => {
|
|
const existing = prev.find((notifier) => notifier._id === id)
|
|
const merged = existing
|
|
? { ...existing, ...updatedData }
|
|
: { _id: id, ...updatedData }
|
|
|
|
if (!notifierMatchesFilter(merged)) {
|
|
return prev.filter((notifier) => notifier._id !== id)
|
|
}
|
|
|
|
if (existing) {
|
|
return prev.map((notifier) =>
|
|
notifier._id === id ? merged : notifier
|
|
)
|
|
}
|
|
|
|
return [...prev, merged]
|
|
})
|
|
|
|
setCurrentUserNotifier((prev) => {
|
|
if (prev?._id === id) {
|
|
const merged = { ...prev, ...updatedData }
|
|
return notifierMatchesFilter(merged) ? merged : null
|
|
}
|
|
|
|
const userRef = updatedData?.user?._id ?? updatedData?.user
|
|
if (
|
|
userRef === userProfile?._id &&
|
|
notifierMatchesFilter({ _id: id, ...updatedData })
|
|
) {
|
|
return { _id: id, ...updatedData }
|
|
}
|
|
|
|
return prev
|
|
})
|
|
},
|
|
[notifierMatchesFilter, userProfile?._id]
|
|
)
|
|
|
|
updateEventHandlerRef.current = updateEventHandler
|
|
|
|
useEffect(() => {
|
|
if (connected !== true || !objectId || !type) return
|
|
|
|
const unsubscribe = subscribeToObjectTypeUpdates(
|
|
'userNotifier',
|
|
subscriptionFilter,
|
|
() => {
|
|
loadNotifierStateRef.current(true)
|
|
if (popoverOpenRef.current) {
|
|
loadAllNotifiersRef.current(true)
|
|
}
|
|
}
|
|
)
|
|
subscribeToObjectTypeUpdatesRef.current = unsubscribe
|
|
|
|
return () => {
|
|
if (unsubscribe) unsubscribe()
|
|
if (subscribeToObjectTypeUpdatesRef.current === unsubscribe) {
|
|
subscribeToObjectTypeUpdatesRef.current = null
|
|
}
|
|
}
|
|
}, [connected, subscriptionFilter, subscribeToObjectTypeUpdates, objectId, type])
|
|
|
|
const subscribedNotifierIds = useMemo(() => {
|
|
const ids = new Set(
|
|
allNotifiers.map((notifier) => notifier._id).filter(Boolean)
|
|
)
|
|
if (currentUserNotifier?._id) {
|
|
ids.add(currentUserNotifier._id)
|
|
}
|
|
return [...ids]
|
|
}, [allNotifiers, currentUserNotifier])
|
|
|
|
useEffect(() => {
|
|
if (connected !== true) return
|
|
|
|
const newNotifierIds = subscribedNotifierIds.filter(
|
|
(id) => !subscribedIdsRef.current.includes(id)
|
|
)
|
|
|
|
newNotifierIds.forEach((itemId) => {
|
|
const unsubscribe = subscribeToObjectUpdates(
|
|
itemId?.toLowerCase(),
|
|
'userNotifier',
|
|
(updateData) => updateEventHandlerRef.current(itemId, updateData)
|
|
)
|
|
subscribedIdsRef.current.push(itemId)
|
|
if (unsubscribe) {
|
|
unsubscribesRef.current.push(unsubscribe)
|
|
}
|
|
})
|
|
|
|
const notifierIdsToUnsubscribe = subscribedIdsRef.current.filter(
|
|
(id) => !subscribedNotifierIds.includes(id)
|
|
)
|
|
|
|
notifierIdsToUnsubscribe.forEach((itemId) => {
|
|
const index = subscribedIdsRef.current.indexOf(itemId)
|
|
if (index > -1) {
|
|
subscribedIdsRef.current.splice(index, 1)
|
|
const unsubscribe = unsubscribesRef.current[index]
|
|
if (unsubscribe) {
|
|
unsubscribe()
|
|
}
|
|
unsubscribesRef.current.splice(index, 1)
|
|
}
|
|
})
|
|
}, [subscribedNotifierIds, connected, subscribeToObjectUpdates])
|
|
|
|
useEffect(() => {
|
|
return () => {
|
|
unsubscribesRef.current.forEach((unsubscribe) => {
|
|
if (unsubscribe) unsubscribe()
|
|
})
|
|
unsubscribesRef.current = []
|
|
subscribedIdsRef.current = []
|
|
|
|
if (subscribeToObjectTypeUpdatesRef.current) {
|
|
subscribeToObjectTypeUpdatesRef.current()
|
|
subscribeToObjectTypeUpdatesRef.current = null
|
|
}
|
|
}
|
|
}, [])
|
|
|
|
const handleClick = useCallback(async () => {
|
|
if (!authReady || !objectId || !type || loading) return
|
|
|
|
setLoading(true)
|
|
try {
|
|
const enabled = await toggleUserNotifier(objectId, type)
|
|
if (enabled) {
|
|
const { data } = await fetchUserNotifiersForObject(objectId, type)
|
|
setCurrentUserNotifier(data?.[0] ?? null)
|
|
} else {
|
|
setCurrentUserNotifier(null)
|
|
}
|
|
if (popoverOpen) {
|
|
const { data } = await fetchAllUserNotifiersForObject(objectId, type)
|
|
setAllNotifiers(data || [])
|
|
}
|
|
message.success(
|
|
enabled
|
|
? 'Notifications enabled for this object'
|
|
: 'Notifications disabled for this object'
|
|
)
|
|
} catch (error) {
|
|
console.error('Error toggling user notifier:', error)
|
|
message.error('Failed to update notifications')
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}, [authReady, objectId, type, loading, popoverOpen, toggleUserNotifier, fetchUserNotifiersForObject, fetchAllUserNotifiersForObject])
|
|
|
|
const getUserDisplayName = useCallback((user) => {
|
|
if (!user) return 'Unknown'
|
|
return (
|
|
user.name ||
|
|
user.username ||
|
|
`${user.firstName || ''} ${user.lastName || ''}`.trim() ||
|
|
user.email ||
|
|
'Unknown'
|
|
)
|
|
}, [])
|
|
|
|
const isCurrentUser = useCallback((user) => user?._id === userProfile?._id, [userProfile?._id])
|
|
|
|
const handleEmailToggle = useCallback(async (item) => {
|
|
if (!isCurrentUser(item.user) || emailTogglingId) return
|
|
setEmailTogglingId(item._id)
|
|
const newEmail = !item.email
|
|
try {
|
|
const result = await editUserNotifier(item._id, { email: newEmail })
|
|
if (result) {
|
|
setAllNotifiers((prev) =>
|
|
prev.map((n) =>
|
|
n._id === item._id ? { ...n, email: result.email ?? newEmail } : n
|
|
)
|
|
)
|
|
setCurrentUserNotifier((prev) =>
|
|
prev?._id === item._id
|
|
? { ...prev, email: result.email ?? newEmail }
|
|
: prev
|
|
)
|
|
message.success(
|
|
(result.email ?? newEmail)
|
|
? 'Email notifications enabled'
|
|
: 'Email notifications disabled'
|
|
)
|
|
}
|
|
} catch (error) {
|
|
console.error('Error toggling email:', error)
|
|
message.error('Failed to update email notifications')
|
|
} finally {
|
|
setEmailTogglingId(null)
|
|
}
|
|
}, [isCurrentUser, emailTogglingId, editUserNotifier])
|
|
|
|
const popoverContent = useMemo(
|
|
() => (
|
|
<Flex
|
|
vertical
|
|
justify='center'
|
|
style={{ minWidth: 240, minHeight: 25 }}
|
|
gap={'4px'}
|
|
>
|
|
{popoverLoading ? (
|
|
<Space size={'small'}>
|
|
<LoadingOutlined />
|
|
<Text style={{ margin: 0 }}>Loading, please wait...</Text>
|
|
</Space>
|
|
) : allNotifiers.length === 0 ? (
|
|
<Space size={'small'}>
|
|
<Text style={{ margin: 0 }} type='secondary'>
|
|
<InfoCircleIcon />
|
|
</Text>
|
|
<Text style={{ margin: 0 }} type='secondary'>
|
|
No users subscribed.
|
|
</Text>
|
|
</Space>
|
|
) : (
|
|
<>
|
|
{[...allNotifiers]
|
|
.sort(
|
|
(a, b) =>
|
|
(isCurrentUser(b.user) ? 1 : 0) -
|
|
(isCurrentUser(a.user) ? 1 : 0)
|
|
)
|
|
.map((item) => (
|
|
<Flex key={item._id} justify='space-between' align='center'>
|
|
<Flex align='center' gap={'6px'}>
|
|
<UserOutlined />
|
|
<Text>{getUserDisplayName(item.user)}</Text>
|
|
{isCurrentUser(item.user) && (
|
|
<Text type='secondary'> (you)</Text>
|
|
)}
|
|
</Flex>
|
|
<Space size={'small'}>
|
|
<Button
|
|
type='text'
|
|
icon={
|
|
item.email ? (
|
|
<MailCheckIcon
|
|
style={{
|
|
color: 'var(--color-primary)'
|
|
}}
|
|
/>
|
|
) : (
|
|
<MailIcon />
|
|
)
|
|
}
|
|
size='small'
|
|
disabled={!isCurrentUser(item.user)}
|
|
loading={emailTogglingId === item._id}
|
|
onClick={() => handleEmailToggle(item)}
|
|
/>
|
|
</Space>
|
|
</Flex>
|
|
))}
|
|
</>
|
|
)}
|
|
</Flex>
|
|
),
|
|
[popoverLoading, allNotifiers, emailTogglingId, isCurrentUser, getUserDisplayName, handleEmailToggle]
|
|
)
|
|
|
|
return (
|
|
<Popover
|
|
content={popoverContent}
|
|
title={null}
|
|
trigger='hover'
|
|
placement='bottomLeft'
|
|
arrow={false}
|
|
open={popoverOpen}
|
|
onOpenChange={setPopoverOpen}
|
|
styles={{ body: { padding: '10px 12.5px 10px 15px' } }}
|
|
>
|
|
<Button
|
|
{...buttonProps}
|
|
icon={
|
|
<BellIcon
|
|
style={{
|
|
color: isNotifying ? 'var(--color-warning)' : undefined
|
|
}}
|
|
/>
|
|
}
|
|
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,
|
|
size: PropTypes.string
|
|
}
|
|
|
|
export default UserNotifierToggle
|