diff --git a/src/components/Dashboard/common/DocumentPrintButton.jsx b/src/components/Dashboard/common/DocumentPrintButton.jsx
index d26cb4a4..6e6d4288 100644
--- a/src/components/Dashboard/common/DocumentPrintButton.jsx
+++ b/src/components/Dashboard/common/DocumentPrintButton.jsx
@@ -21,7 +21,12 @@ const DocumentPrintButton = ({
disabled = false,
...buttonProps
}) => {
- const { fetchObjects } = useContext(ApiServerContext)
+ const {
+ fetchObjects,
+ connected,
+ subscribeToObjectUpdates,
+ subscribeToObjectTypeUpdates
+ } = useContext(ApiServerContext)
const fetchObjectsRef = useRef(fetchObjects)
fetchObjectsRef.current = fetchObjects
@@ -30,34 +35,59 @@ const DocumentPrintButton = ({
const [loading, setLoading] = useState(false)
const [newDocumentJobOpen, setNewDocumentJobOpen] = useState(false)
+ const subscribedIdsRef = useRef([])
+ const unsubscribesRef = useRef([])
+ const subscribeToObjectTypeUpdatesRef = useRef(null)
+ const updateEventHandlerRef = useRef()
+
const { token } = useContext(AuthContext)
- // Get the model by name
- //const model = getModelByName(type)
+ const subscriptionFilter = useMemo(
+ () => ({
+ objectType: type,
+ global: false,
+ active: true
+ }),
+ [type]
+ )
- const loadDocumentTemplates = useCallback(async () => {
- if (!type || token == null) return
+ const templateMatchesFilter = useCallback(
+ (template) =>
+ template?.objectType === type &&
+ template?.global === false &&
+ template?.active === true,
+ [type]
+ )
- setLoading(true)
- try {
- const result = await fetchObjectsRef.current('documentTemplate', {
- filter: {
- objectType: type,
- global: false,
- active: true
- },
- limit: 100 // Get more templates to show in dropdown
- })
+ const loadDocumentTemplates = useCallback(
+ async (silent = false) => {
+ if (!type || token == null) return
- if (result && result.data) {
- setDocumentTemplates(result.data)
+ if (!silent) {
+ setLoading(true)
}
- } catch (error) {
- console.error('Error fetching document templates:', error)
- } finally {
- setLoading(false)
- }
- }, [type, token])
+ try {
+ const result = await fetchObjectsRef.current('documentTemplate', {
+ filter: subscriptionFilter,
+ limit: 100 // Get more templates to show in dropdown
+ })
+
+ if (result && result.data) {
+ setDocumentTemplates(result.data)
+ }
+ } catch (error) {
+ console.error('Error fetching document templates:', error)
+ } finally {
+ if (!silent) {
+ setLoading(false)
+ }
+ }
+ },
+ [type, token, subscriptionFilter]
+ )
+
+ const loadDocumentTemplatesRef = useRef(loadDocumentTemplates)
+ loadDocumentTemplatesRef.current = loadDocumentTemplates
// Stable key from objectData._id (excludes _isEditing so toggling edit mode doesn't trigger template reload)
const objectKey = useMemo(() => {
@@ -70,6 +100,103 @@ const DocumentPrintButton = ({
loadDocumentTemplates()
}, [objectKey, token, type, loadDocumentTemplates])
+ const updateEventHandler = useCallback(
+ (id, updatedData) => {
+ setDocumentTemplates((prev) => {
+ const existing = prev.find((template) => template._id === id)
+ const merged = existing
+ ? { ...existing, ...updatedData }
+ : { _id: id, ...updatedData }
+
+ if (!templateMatchesFilter(merged)) {
+ return prev.filter((template) => template._id !== id)
+ }
+
+ if (existing) {
+ return prev.map((template) =>
+ template._id === id ? merged : template
+ )
+ }
+
+ return [...prev, merged]
+ })
+ },
+ [templateMatchesFilter]
+ )
+
+ updateEventHandlerRef.current = updateEventHandler
+
+ useEffect(() => {
+ if (connected !== true) return
+
+ const unsubscribe = subscribeToObjectTypeUpdates(
+ 'documentTemplate',
+ subscriptionFilter,
+ () => loadDocumentTemplatesRef.current(true)
+ )
+ subscribeToObjectTypeUpdatesRef.current = unsubscribe
+
+ return () => {
+ if (unsubscribe) unsubscribe()
+ if (subscribeToObjectTypeUpdatesRef.current === unsubscribe) {
+ subscribeToObjectTypeUpdatesRef.current = null
+ }
+ }
+ }, [connected, subscriptionFilter, subscribeToObjectTypeUpdates])
+
+ useEffect(() => {
+ if (connected !== true) return
+
+ const templateIds = documentTemplates.map((template) => template._id).filter(Boolean)
+
+ const newTemplateIds = templateIds.filter(
+ (id) => !subscribedIdsRef.current.includes(id)
+ )
+
+ newTemplateIds.forEach((itemId) => {
+ const unsubscribe = subscribeToObjectUpdates(
+ itemId?.toLowerCase(),
+ 'documentTemplate',
+ (updateData) => updateEventHandlerRef.current(itemId, updateData)
+ )
+ subscribedIdsRef.current.push(itemId)
+ if (unsubscribe) {
+ unsubscribesRef.current.push(unsubscribe)
+ }
+ })
+
+ const templateIdsToUnsubscribe = subscribedIdsRef.current.filter(
+ (id) => !templateIds.includes(id)
+ )
+
+ templateIdsToUnsubscribe.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)
+ }
+ })
+ }, [documentTemplates, connected, subscribeToObjectUpdates])
+
+ useEffect(() => {
+ return () => {
+ unsubscribesRef.current.forEach((unsubscribe) => {
+ if (unsubscribe) unsubscribe()
+ })
+ unsubscribesRef.current = []
+ subscribedIdsRef.current = []
+
+ if (subscribeToObjectTypeUpdatesRef.current) {
+ subscribeToObjectTypeUpdatesRef.current()
+ subscribeToObjectTypeUpdatesRef.current = null
+ }
+ }
+ }, [])
+
// Handle template selection
const handleTemplateSelect = (template) => {
setCurrentDocumentTemplate(template)
@@ -78,13 +205,54 @@ const DocumentPrintButton = ({
// This could open a print dialog, navigate to a print page, etc.
}
- // Create dropdown menu items
- const menuItems = documentTemplates.map((template) => ({
- key: template._id,
- label: template.name,
- icon: ,
- onClick: () => handleTemplateSelect(template)
- }))
+ // Group templates by tag for nested dropdown menu; untagged templates at root
+ const menuItems = useMemo(() => {
+ const templatesByTag = new Map()
+ const untaggedTemplates = []
+
+ const toTemplateMenuItem = (template) => ({
+ key: template._id,
+ label: template.name,
+ icon: ,
+ onClick: () => handleTemplateSelect(template)
+ })
+
+ for (const template of documentTemplates) {
+ const templateTags = Array.isArray(template.tags)
+ ? template.tags.filter(Boolean)
+ : []
+
+ if (templateTags.length === 0) {
+ untaggedTemplates.push(template)
+ continue
+ }
+
+ for (const tag of templateTags) {
+ const templates = templatesByTag.get(tag) ?? []
+ templates.push(template)
+ templatesByTag.set(tag, templates)
+ }
+ }
+
+ const taggedMenuItems = [...templatesByTag.keys()]
+ .sort((a, b) => a.localeCompare(b))
+ .map((tag) => ({
+ key: tag,
+ label: tag,
+ children: templatesByTag
+ .get(tag)
+ .slice()
+ .sort((a, b) => a.name.localeCompare(b.name))
+ .map(toTemplateMenuItem)
+ }))
+
+ const rootUntaggedItems = untaggedTemplates
+ .slice()
+ .sort((a, b) => a.name.localeCompare(b.name))
+ .map(toTemplateMenuItem)
+
+ return [...rootUntaggedItems, ...taggedMenuItems]
+ }, [documentTemplates])
// If no templates available, show disabled state
if (documentTemplates.length === 0 && !loading) {
diff --git a/src/components/Dashboard/common/UserNotifierToggle.jsx b/src/components/Dashboard/common/UserNotifierToggle.jsx
index b683fd2c..afb5521b 100644
--- a/src/components/Dashboard/common/UserNotifierToggle.jsx
+++ b/src/components/Dashboard/common/UserNotifierToggle.jsx
@@ -23,10 +23,13 @@ const UserNotifierToggle = memo(({
toggleUserNotifier,
editUserNotifier,
fetchUserNotifiersForObject,
- fetchAllUserNotifiersForObject
+ fetchAllUserNotifiersForObject,
+ connected,
+ subscribeToObjectUpdates,
+ subscribeToObjectTypeUpdates
} = useContext(ApiServerContext)
const { userProfile, token, authInitialized } = useContext(AuthContext)
- const [isNotifying, setIsNotifying] = useState(false)
+ const [currentUserNotifier, setCurrentUserNotifier] = useState(null)
const [loading, setLoading] = useState(false)
const [initialLoad, setInitialLoad] = useState(true)
const [allNotifiers, setAllNotifiers] = useState([])
@@ -34,8 +37,32 @@ const UserNotifierToggle = memo(({
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,
@@ -46,42 +73,196 @@ const UserNotifierToggle = memo(({
fetchAllUserNotifiersForObject
}
- useEffect(() => {
- if (!authReady || !objectId || !type) return
+ const loadNotifierState = useCallback(
+ async (silent = false) => {
+ if (!authReady || !objectId || !type) return
- const loadNotifierState = async () => {
- setInitialLoad(true)
+ if (!silent) {
+ setInitialLoad(true)
+ }
try {
- const { data } = await apiRef.current.fetchUserNotifiersForObject(objectId, type)
- setIsNotifying(data?.length > 0)
+ const { data } = await apiRef.current.fetchUserNotifiersForObject(
+ objectId,
+ type
+ )
+ setCurrentUserNotifier(data?.[0] ?? null)
} catch (error) {
console.error('Error fetching user notifier state:', error)
} finally {
- setInitialLoad(false)
+ if (!silent) {
+ setInitialLoad(false)
+ }
}
- }
+ },
+ [authReady, objectId, type]
+ )
- loadNotifierState()
- }, [authReady, objectId, type])
+ const loadAllNotifiers = useCallback(
+ async (silent = false) => {
+ if (!authReady || !objectId || !type) return
- useEffect(() => {
- if (!authReady || !objectId || !type || !popoverOpen) return
-
- const loadAllNotifiers = async () => {
- setPopoverLoading(true)
+ if (!silent) {
+ setPopoverLoading(true)
+ }
try {
- const { data } = await apiRef.current.fetchAllUserNotifiersForObject(objectId, type)
+ const { data } = await apiRef.current.fetchAllUserNotifiersForObject(
+ objectId,
+ type
+ )
setAllNotifiers(data || [])
} catch (error) {
console.error('Error fetching all user notifiers:', error)
setAllNotifiers([])
} finally {
- setPopoverLoading(false)
+ 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])
- loadAllNotifiers()
- }, [authReady, objectId, type, popoverOpen])
+ 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
@@ -89,7 +270,12 @@ const UserNotifierToggle = memo(({
setLoading(true)
try {
const enabled = await toggleUserNotifier(objectId, type)
- setIsNotifying(enabled)
+ 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 || [])
@@ -105,7 +291,7 @@ const UserNotifierToggle = memo(({
} finally {
setLoading(false)
}
- }, [authReady, objectId, type, loading, popoverOpen, toggleUserNotifier, fetchAllUserNotifiersForObject])
+ }, [authReady, objectId, type, loading, popoverOpen, toggleUserNotifier, fetchUserNotifiersForObject, fetchAllUserNotifiersForObject])
const getUserDisplayName = useCallback((user) => {
if (!user) return 'Unknown'
@@ -132,6 +318,11 @@ const UserNotifierToggle = memo(({
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'