Tom Butcher 483682ce44
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
Add Email Account and Email Message Management Components
- Introduced new EmailAccounts and EmailMessages components for managing email accounts and messages.
- Implemented ObjectTable for displaying data with filtering and sorting capabilities.
- Integrated InfoActionButtons for enhanced user interactions across various management functionalities.
- Refactored existing components to replace DocumentPrintButton with InfoActionButtons for consistency in action handling.
- Added EmailAccountInfo component for detailed view and management of individual email accounts.
2026-09-12 21:40:18 +01:00

320 lines
8.6 KiB
JavaScript

import PropTypes from 'prop-types'
import {
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState
} from 'react'
import { Modal } from 'antd'
import ObjectInfo from '../../common/ObjectInfo'
import NewObjectForm from '../../common/NewObjectForm'
import WizardView from '../../common/WizardView'
import TemplatePreview from '../../common/TemplatePreview'
import ProgressDisplay from '../../common/ProgressDisplay'
import { ApiServerContext } from '../../context/ApiServerContext'
import { resolveEmailRecipients } from '../../utils/emailRecipients'
const progressByState = {
queued: 10,
rendering: 35,
sending: 70,
sent: 100,
error: 100
}
const EMPTY_RECIPIENT_CANDIDATES = []
const EMPTY_DEFAULT_VALUES = {}
const EmailWizardContent = ({
accounts,
defaultCandidates,
handleSubmit,
submitLoading,
objectData,
setObjectData,
form,
formValid,
onCreated
}) => {
const { fetchObject } = useContext(ApiServerContext)
const [candidates, setCandidates] = useState(defaultCandidates)
useEffect(() => {
if (defaultCandidates.length) {
setCandidates(defaultCandidates)
return
}
let cancelled = false
resolveEmailRecipients({
type: objectData?.objectType,
objectData: objectData?.object,
fetchObject
}).then((result) => {
if (!cancelled) setCandidates(result)
})
return () => {
cancelled = true
}
}, [
defaultCandidates,
fetchObject,
objectData?.object,
objectData?.objectType
])
useEffect(() => {
setObjectData((previous) => ({
...previous,
_recipientCandidates: candidates
}))
}, [candidates, setObjectData])
useEffect(() => {
const currentAccount = objectData?.emailAccount
const account =
accounts.find(
(entry) => entry._id === (currentAccount?._id || currentAccount)
) || accounts[0]
if (!account || objectData?.emailAccount) return
form.setFieldsValue({ emailAccount: account, fromEmail: account.fromEmail })
setObjectData((previous) => ({
...previous,
emailAccount: account,
fromEmail: account.fromEmail
}))
}, [accounts, form, objectData?.emailAccount, setObjectData])
useEffect(() => {
if (!candidates.length || objectData?.recipientEmail) return
const candidate = candidates[0]
const values = {
recipientEmail: candidate.email,
recipientType: candidate.recipientType,
recipient: candidate.recipient
}
form.setFieldsValue(values)
setObjectData((previous) => ({ ...previous, ...values }))
}, [candidates, form, objectData?.recipientEmail, setObjectData])
return (
<WizardView
steps={[
{
title: 'Required',
key: 'required',
content: (
<ObjectInfo
type='emailMessage'
column={1}
visibleProperties={{
name: true,
objectType: true,
object: true,
emailTemplate: true,
emailAccount: true,
recipientEmail: true,
recipientType: true,
recipient: true,
fromEmail: true
}}
bordered={false}
labelWidth={115}
isEditing
objectData={objectData}
/>
)
}
]}
submitText='Send'
title='Send Email'
formValid={
formValid && Boolean(objectData?.recipientEmail) && accounts.length > 0
}
loading={submitLoading}
sideBarGrow
sideBar={
<div
style={{
height: 'calc(100vh - 240px)',
flexGrow: 1,
minWidth: 0
}}
>
<TemplatePreview
objectData={objectData?.object}
template={objectData?.emailTemplate}
templateType='emailTemplate'
capabilities={{
htmlOnly: true,
widthControl: true,
defaultWidth: 600
}}
/>
</div>
}
onSubmit={async () => {
const result = await handleSubmit()
if (result?._id) onCreated(result)
}}
/>
)
}
EmailWizardContent.propTypes = {
accounts: PropTypes.array.isRequired,
defaultCandidates: PropTypes.array.isRequired,
handleSubmit: PropTypes.func.isRequired,
submitLoading: PropTypes.bool,
objectData: PropTypes.object,
setObjectData: PropTypes.func.isRequired,
form: PropTypes.object.isRequired,
formValid: PropTypes.bool,
onCreated: PropTypes.func.isRequired
}
const NewEmailMessage = ({
onOk,
defaultValues = EMPTY_DEFAULT_VALUES,
recipientCandidates = EMPTY_RECIPIENT_CANDIDATES
}) => {
const {
fetchObject,
fetchObjects,
connected,
subscribeToObjectUpdates,
subscribeToObjectTypeUpdates
} = useContext(ApiServerContext)
const [accounts, setAccounts] = useState([])
const [sendingMessage, setSendingMessage] = useState(null)
const fetchObjectRef = useRef(fetchObject)
const fetchObjectsRef = useRef(fetchObjects)
const fetchedMessageIdRef = useRef(null)
fetchObjectRef.current = fetchObject
fetchObjectsRef.current = fetchObjects
const initialValues = useMemo(
() => ({ read: false, ...defaultValues }),
[defaultValues]
)
const loadAccounts = useCallback(async () => {
const result = await fetchObjectsRef.current('emailAccount', {
filter: { active: true },
limit: 100
})
setAccounts(Array.isArray(result?.data) ? result.data : [])
}, [])
useEffect(() => {
loadAccounts()
}, [loadAccounts])
useEffect(() => {
if (!connected) return undefined
return subscribeToObjectTypeUpdates(
'emailAccount',
{ active: true },
loadAccounts
)
}, [connected, loadAccounts, subscribeToObjectTypeUpdates])
useEffect(() => {
if (!connected) return undefined
const unsubscribes = accounts
.filter((account) => account?._id)
.map((account) =>
subscribeToObjectUpdates(
account._id.toLowerCase(),
'emailAccount',
(update) => {
const merged = { ...account, ...update }
setAccounts((previous) =>
merged.active === true
? previous.map((entry) =>
entry._id === account._id ? merged : entry
)
: previous.filter((entry) => entry._id !== account._id)
)
}
)
)
return () => unsubscribes.forEach((unsubscribe) => unsubscribe?.())
}, [accounts, connected, subscribeToObjectUpdates])
useEffect(() => {
const id = sendingMessage?._id
if (!id) return undefined
let cancelled = false
if (fetchedMessageIdRef.current !== id) {
fetchedMessageIdRef.current = id
fetchObjectRef.current(id, 'emailMessage').then((current) => {
if (!cancelled && current?._id) {
setSendingMessage((previous) => ({ ...previous, ...current }))
}
})
}
const unsubscribe =
connected === true
? subscribeToObjectUpdates(id.toLowerCase(), 'emailMessage', (update) =>
setSendingMessage((previous) => ({ ...previous, ...update }))
)
: undefined
return () => {
cancelled = true
unsubscribe?.()
}
}, [connected, sendingMessage?._id, subscribeToObjectUpdates])
useEffect(() => {
if (sendingMessage?.state?.type === 'sent') onOk?.(sendingMessage)
}, [onOk, sendingMessage])
return (
<>
<NewObjectForm type='emailMessage' defaultValues={initialValues}>
{(formProps) => (
<EmailWizardContent
{...formProps}
accounts={accounts}
defaultCandidates={recipientCandidates}
onCreated={setSendingMessage}
/>
)}
</NewObjectForm>
<Modal
open={Boolean(sendingMessage)}
footer={null}
closable={sendingMessage?.state?.type === 'error'}
onCancel={() => setSendingMessage(null)}
maskClosable={false}
centered
destroyOnHidden
zIndex={2000}
width={360}
getContainer={() => document.body}
>
<ProgressDisplay
percent={progressByState[sendingMessage?.state?.type] || 0}
status={
sendingMessage?.state?.type === 'error' ? 'exception' : 'active'
}
>
{sendingMessage?.state?.message ||
`Email ${sendingMessage?.state?.type || 'queued'}...`}
</ProgressDisplay>
</Modal>
</>
)
}
NewEmailMessage.propTypes = {
onOk: PropTypes.func,
defaultValues: PropTypes.object,
recipientCandidates: PropTypes.array
}
export default NewEmailMessage