farmcontrol-ui/src/components/Dashboard/common/DocumentPrintButton.jsx
Tom Butcher 835c287810 Enhance DocumentPrintButton and UserNotifierToggle with Subscription Logic
- 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.
2026-09-01 11:50:47 +01:00

324 lines
8.8 KiB
JavaScript

import PropTypes from 'prop-types'
import {
useState,
useEffect,
useContext,
useCallback,
useMemo,
useRef
} from 'react'
// import { getModelByName } from '../../../database/ObjectModels'
import DocumentPrinterIcon from '../../Icons/DocumentPrinterIcon'
import { Button, Dropdown, Modal } from 'antd'
import { ApiServerContext } from '../context/ApiServerContext'
import DocumentTemplateIcon from '../../Icons/DocumentTemplateIcon'
import NewDocumentJob from '../Management/DocumentJobs/NewDocumentJob'
import { AuthContext } from '../context/AuthContext'
const DocumentPrintButton = ({
type,
objectData,
disabled = false,
...buttonProps
}) => {
const {
fetchObjects,
connected,
subscribeToObjectUpdates,
subscribeToObjectTypeUpdates
} = useContext(ApiServerContext)
const fetchObjectsRef = useRef(fetchObjects)
fetchObjectsRef.current = fetchObjects
const [documentTemplates, setDocumentTemplates] = useState([])
const [currentDocumentTemplate, setCurrentDocumentTemplate] = useState(null)
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)
const subscriptionFilter = useMemo(
() => ({
objectType: type,
global: false,
active: true
}),
[type]
)
const templateMatchesFilter = useCallback(
(template) =>
template?.objectType === type &&
template?.global === false &&
template?.active === true,
[type]
)
const loadDocumentTemplates = useCallback(
async (silent = false) => {
if (!type || token == null) return
if (!silent) {
setLoading(true)
}
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(() => {
if (!objectData) return null
return objectData._id ?? objectData.id ?? null
}, [objectData])
// Fetch document templates when component mounts or objectData, type or token changes
useEffect(() => {
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)
setNewDocumentJobOpen(true)
// TODO: Implement the actual printing logic here
// This could open a print dialog, navigate to a print page, etc.
}
// 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: <DocumentTemplateIcon />,
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) {
return (
<Button
{...buttonProps}
icon={<DocumentPrinterIcon />}
disabled={true}
title='No document templates available for this object type'
/>
)
}
return (
<>
<Dropdown
menu={{
items: menuItems,
loading: loading
}}
trigger={['hover']}
disabled={disabled || loading}
>
<Button
{...buttonProps}
icon={<DocumentPrinterIcon />}
disabled={disabled || loading}
loading={loading}
title={loading ? 'Loading templates...' : 'Print document'}
/>
</Dropdown>
<Modal
open={newDocumentJobOpen}
onCancel={() => setNewDocumentJobOpen(false)}
footer={null}
destroyOnHidden={true}
width={{
xs: '100%',
sm: '100%',
md: '100%',
lg: '90%',
xl: '80%',
xxl: '80%'
}}
>
<NewDocumentJob
onOk={() => {
setNewDocumentJobOpen(false)
}}
reset={!newDocumentJobOpen}
defaultValues={{
objectType: type,
object: objectData,
documentTemplate: currentDocumentTemplate
}}
/>
</Modal>
</>
)
}
DocumentPrintButton.propTypes = {
type: PropTypes.string.isRequired,
disabled: PropTypes.bool,
objectData: PropTypes.object.isRequired
}
export default DocumentPrintButton