Enhance Email Messaging Components with Recipients Management and UI Improvements
- Introduced ObjectChildCards component to manage recipients in the NewEmailMessage and EmailMessageInfo components, allowing for better organization and display of recipient information. - Updated EmailMessageInfo to include a collapsible section for recipients, enhancing user experience by providing clear visibility of recipient details. - Refactored email recipient handling in NewEmailMessage to utilize candidatesToRecipients for improved recipient management and validation. - Enhanced EmailSendButton to accommodate the new recipients structure, ensuring proper data flow and functionality. - Improved utility functions for email recipient resolution, streamlining the process of fetching and managing recipient data.
This commit is contained in:
parent
c9751dd5ef
commit
da2881d505
@ -20,7 +20,8 @@ import { parser as ejsParser } from './ejs.parser.js'
|
|||||||
import {
|
import {
|
||||||
fcTemplateElements,
|
fcTemplateElements,
|
||||||
fcTemplateAttributes,
|
fcTemplateAttributes,
|
||||||
getFcTemplateHelpers
|
getFcTemplateHelpers,
|
||||||
|
placeholderCompletionSource
|
||||||
} from './schema.js'
|
} from './schema.js'
|
||||||
import { normalizeTypedScope } from '../javascriptLang/typedScope.js'
|
import { normalizeTypedScope } from '../javascriptLang/typedScope.js'
|
||||||
|
|
||||||
@ -140,6 +141,7 @@ export function fcTemplateLang(options = {}) {
|
|||||||
} = options
|
} = options
|
||||||
const jsScope = buildJsScope(autoCompleteObject, templateType)
|
const jsScope = buildJsScope(autoCompleteObject, templateType)
|
||||||
const completeResources = resourceCompletionSource(resourceNames)
|
const completeResources = resourceCompletionSource(resourceNames)
|
||||||
|
const completePlaceholders = placeholderCompletionSource()
|
||||||
|
|
||||||
return new LanguageSupport(fcTemplateLanguage, [
|
return new LanguageSupport(fcTemplateLanguage, [
|
||||||
xmlLanguage.data.of({
|
xmlLanguage.data.of({
|
||||||
@ -149,6 +151,8 @@ export function fcTemplateLang(options = {}) {
|
|||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
xmlLanguage.data.of({ autocomplete: completeResources }),
|
xmlLanguage.data.of({ autocomplete: completeResources }),
|
||||||
|
xmlLanguage.data.of({ autocomplete: completePlaceholders }),
|
||||||
|
fcTemplateLanguage.data.of({ autocomplete: completePlaceholders }),
|
||||||
javascriptLanguage.data.of({ autocomplete: completeResources }),
|
javascriptLanguage.data.of({ autocomplete: completeResources }),
|
||||||
autoCloseTags,
|
autoCloseTags,
|
||||||
...javascriptCompletionSupport(jsScope)
|
...javascriptCompletionSupport(jsScope)
|
||||||
|
|||||||
@ -8,8 +8,10 @@ import useCollapseState from '../../hooks/useCollapseState.jsx'
|
|||||||
import NotesPanel from '../../common/NotesPanel.jsx'
|
import NotesPanel from '../../common/NotesPanel.jsx'
|
||||||
import InfoCollapse from '../../common/InfoCollapse.jsx'
|
import InfoCollapse from '../../common/InfoCollapse.jsx'
|
||||||
import ObjectInfo from '../../common/ObjectInfo.jsx'
|
import ObjectInfo from '../../common/ObjectInfo.jsx'
|
||||||
|
import ObjectProperty from '../../common/ObjectProperty.jsx'
|
||||||
import ViewButton from '../../common/ViewButton.jsx'
|
import ViewButton from '../../common/ViewButton.jsx'
|
||||||
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
|
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
|
||||||
|
import EmailMessageIcon from '../../../Icons/EmailMessageIcon.jsx'
|
||||||
import NoteIcon from '../../../Icons/NoteIcon.jsx'
|
import NoteIcon from '../../../Icons/NoteIcon.jsx'
|
||||||
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
|
import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx'
|
||||||
import ObjectForm from '../../common/ObjectForm.jsx'
|
import ObjectForm from '../../common/ObjectForm.jsx'
|
||||||
@ -21,7 +23,7 @@ import ObjectTable from '../../common/ObjectTable.jsx'
|
|||||||
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
|
||||||
import InfoActionButtons from '../../common/InfoActionButtons.jsx'
|
import InfoActionButtons from '../../common/InfoActionButtons.jsx'
|
||||||
import UserNotifierToggle from '../../common/UserNotifierToggle.jsx'
|
import UserNotifierToggle from '../../common/UserNotifierToggle.jsx'
|
||||||
import { getModelByName } from '../../../../database/ObjectModels.js'
|
import { getModelByName, getModelProperty } from '../../../../database/ObjectModels.js'
|
||||||
import ScrollBox from '../../common/ScrollBox.jsx'
|
import ScrollBox from '../../common/ScrollBox.jsx'
|
||||||
import TemplatePreview from '../../common/TemplatePreview.jsx'
|
import TemplatePreview from '../../common/TemplatePreview.jsx'
|
||||||
|
|
||||||
@ -38,6 +40,7 @@ const EmailMessageInfo = () => {
|
|||||||
'EmailMessageInfo',
|
'EmailMessageInfo',
|
||||||
{
|
{
|
||||||
info: true,
|
info: true,
|
||||||
|
recipients: true,
|
||||||
notes: true,
|
notes: true,
|
||||||
auditLogs: false
|
auditLogs: false
|
||||||
}
|
}
|
||||||
@ -87,6 +90,7 @@ const EmailMessageInfo = () => {
|
|||||||
disabled={objectFormState.loading}
|
disabled={objectFormState.loading}
|
||||||
items={[
|
items={[
|
||||||
{ key: 'info', label: 'Email Message Information' },
|
{ key: 'info', label: 'Email Message Information' },
|
||||||
|
{ key: 'recipients', label: 'Recipients' },
|
||||||
{ key: 'notes', label: 'Notes' },
|
{ key: 'notes', label: 'Notes' },
|
||||||
{ key: 'auditLogs', label: 'Audit Logs' }
|
{ key: 'auditLogs', label: 'Audit Logs' }
|
||||||
]}
|
]}
|
||||||
@ -141,10 +145,28 @@ const EmailMessageInfo = () => {
|
|||||||
objectData={objectData}
|
objectData={objectData}
|
||||||
labelWidth='190px'
|
labelWidth='190px'
|
||||||
visibleProperties={{
|
visibleProperties={{
|
||||||
content: false
|
content: false,
|
||||||
|
recipients: false
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</InfoCollapse>
|
</InfoCollapse>
|
||||||
|
<InfoCollapse
|
||||||
|
title='Recipients'
|
||||||
|
icon={<EmailMessageIcon />}
|
||||||
|
active={collapseState.recipients}
|
||||||
|
onToggle={(expanded) =>
|
||||||
|
updateCollapseState('recipients', expanded)
|
||||||
|
}
|
||||||
|
collapseKey='recipients'
|
||||||
|
>
|
||||||
|
<ObjectProperty
|
||||||
|
{...getModelProperty('emailMessage', 'recipients')}
|
||||||
|
isEditing={isEditing}
|
||||||
|
objectData={objectData}
|
||||||
|
loading={loading}
|
||||||
|
size='medium'
|
||||||
|
/>
|
||||||
|
</InfoCollapse>
|
||||||
<InfoCollapse
|
<InfoCollapse
|
||||||
title='Email Message Content'
|
title='Email Message Content'
|
||||||
icon={<InfoCircleIcon />}
|
icon={<InfoCircleIcon />}
|
||||||
|
|||||||
@ -7,23 +7,21 @@ import {
|
|||||||
useRef,
|
useRef,
|
||||||
useState
|
useState
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import { Modal } from 'antd'
|
import { Form, Modal } from 'antd'
|
||||||
import ObjectInfo from '../../common/ObjectInfo'
|
import ObjectInfo from '../../common/ObjectInfo'
|
||||||
|
import ObjectChildCards from '../../common/ObjectChildCards'
|
||||||
import NewObjectForm from '../../common/NewObjectForm'
|
import NewObjectForm from '../../common/NewObjectForm'
|
||||||
import WizardView from '../../common/WizardView'
|
import WizardView from '../../common/WizardView'
|
||||||
import TemplatePreview from '../../common/TemplatePreview'
|
import TemplatePreview from '../../common/TemplatePreview'
|
||||||
import ProgressDisplay from '../../common/ProgressDisplay'
|
import ProgressDisplay from '../../common/ProgressDisplay'
|
||||||
import { ApiServerContext } from '../../context/ApiServerContext'
|
import { ApiServerContext } from '../../context/ApiServerContext'
|
||||||
import { resolveEmailRecipientsForObjects } from '../../utils/emailRecipients'
|
import {
|
||||||
|
candidatesToRecipients,
|
||||||
|
getReferenceId,
|
||||||
|
resolveEmailRecipientsForObjects
|
||||||
|
} from '../../utils/emailRecipients'
|
||||||
import { toTemplateObjectData } from '../../utils/templateObjectData'
|
import { toTemplateObjectData } from '../../utils/templateObjectData'
|
||||||
|
|
||||||
const progressByState = {
|
|
||||||
queued: 10,
|
|
||||||
rendering: 35,
|
|
||||||
sending: 70,
|
|
||||||
sent: 100,
|
|
||||||
error: 100
|
|
||||||
}
|
|
||||||
const EMPTY_RECIPIENT_CANDIDATES = []
|
const EMPTY_RECIPIENT_CANDIDATES = []
|
||||||
const EMPTY_DEFAULT_VALUES = {}
|
const EMPTY_DEFAULT_VALUES = {}
|
||||||
|
|
||||||
@ -40,20 +38,49 @@ const EmailWizardContent = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { fetchObject } = useContext(ApiServerContext)
|
const { fetchObject } = useContext(ApiServerContext)
|
||||||
const [candidates, setCandidates] = useState(defaultCandidates)
|
const [candidates, setCandidates] = useState(defaultCandidates)
|
||||||
|
const [recipientIndex, setRecipientIndex] = useState(0)
|
||||||
|
const recipients = Array.isArray(objectData?.recipients)
|
||||||
|
? objectData.recipients
|
||||||
|
: []
|
||||||
|
const currentRecipient = recipients[recipientIndex] || null
|
||||||
|
const previewObject = useMemo(() => {
|
||||||
|
const selected = currentRecipient?.object
|
||||||
|
const selectedId = getReferenceId(selected)
|
||||||
|
const objects = Array.isArray(objectData?.objects) ? objectData.objects : []
|
||||||
|
if (selectedId) {
|
||||||
|
const match = objects.find((item) => getReferenceId(item) === selectedId)
|
||||||
|
if (match && typeof match === 'object' && !Array.isArray(match)) {
|
||||||
|
return match
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (selected && typeof selected === 'object' && !Array.isArray(selected)) {
|
||||||
|
return selected
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}, [currentRecipient?.object, objectData?.objects])
|
||||||
|
|
||||||
|
const relatedObjects = Array.isArray(objectData?.objects)
|
||||||
|
? objectData.objects
|
||||||
|
: objectData?.object
|
||||||
|
? [objectData.object]
|
||||||
|
: []
|
||||||
|
const objectIdsKey = relatedObjects
|
||||||
|
.map((item) => getReferenceId(item))
|
||||||
|
.filter(Boolean)
|
||||||
|
.join('|')
|
||||||
|
const relatedObjectsRef = useRef(relatedObjects)
|
||||||
|
relatedObjectsRef.current = relatedObjects
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (defaultCandidates.length) {
|
const objects = relatedObjectsRef.current
|
||||||
setCandidates(defaultCandidates)
|
if (!objects.length) {
|
||||||
return
|
setCandidates([])
|
||||||
|
return undefined
|
||||||
}
|
}
|
||||||
let cancelled = false
|
let cancelled = false
|
||||||
resolveEmailRecipientsForObjects({
|
resolveEmailRecipientsForObjects({
|
||||||
type: objectData?.objectType,
|
type: objectData?.objectType,
|
||||||
objects: Array.isArray(objectData?.objects)
|
objects,
|
||||||
? objectData.objects
|
|
||||||
: objectData?.object
|
|
||||||
? [objectData.object]
|
|
||||||
: [],
|
|
||||||
fetchObject
|
fetchObject
|
||||||
}).then((result) => {
|
}).then((result) => {
|
||||||
if (!cancelled) setCandidates(result)
|
if (!cancelled) setCandidates(result)
|
||||||
@ -61,13 +88,7 @@ const EmailWizardContent = ({
|
|||||||
return () => {
|
return () => {
|
||||||
cancelled = true
|
cancelled = true
|
||||||
}
|
}
|
||||||
}, [
|
}, [fetchObject, objectData?.objectType, objectIdsKey])
|
||||||
defaultCandidates,
|
|
||||||
fetchObject,
|
|
||||||
objectData?.object,
|
|
||||||
objectData?.objects,
|
|
||||||
objectData?.objectType
|
|
||||||
])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setObjectData((previous) => ({
|
setObjectData((previous) => ({
|
||||||
@ -76,6 +97,23 @@ const EmailWizardContent = ({
|
|||||||
}))
|
}))
|
||||||
}, [candidates, setObjectData])
|
}, [candidates, setObjectData])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const nextRecipients = candidatesToRecipients(candidates)
|
||||||
|
form.setFieldsValue({ recipients: nextRecipients })
|
||||||
|
setRecipientIndex((index) => (index < nextRecipients.length ? index : 0))
|
||||||
|
setObjectData((previous) => ({
|
||||||
|
...previous,
|
||||||
|
recipients: nextRecipients
|
||||||
|
}))
|
||||||
|
}, [candidates, form, setObjectData])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setObjectData((previous) => {
|
||||||
|
if (previous?.currentRecipient === currentRecipient) return previous
|
||||||
|
return { ...previous, currentRecipient }
|
||||||
|
})
|
||||||
|
}, [currentRecipient, setObjectData])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const currentAccount = objectData?.emailAccount
|
const currentAccount = objectData?.emailAccount
|
||||||
const account =
|
const account =
|
||||||
@ -91,18 +129,6 @@ const EmailWizardContent = ({
|
|||||||
}))
|
}))
|
||||||
}, [accounts, form, objectData?.emailAccount, setObjectData])
|
}, [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 (
|
return (
|
||||||
<WizardView
|
<WizardView
|
||||||
steps={[
|
steps={[
|
||||||
@ -110,32 +136,60 @@ const EmailWizardContent = ({
|
|||||||
title: 'Required',
|
title: 'Required',
|
||||||
key: 'required',
|
key: 'required',
|
||||||
content: (
|
content: (
|
||||||
<ObjectInfo
|
<div>
|
||||||
type='emailMessage'
|
<ObjectInfo
|
||||||
column={1}
|
type='emailMessage'
|
||||||
visibleProperties={{
|
column={1}
|
||||||
name: true,
|
visibleProperties={{
|
||||||
objectType: true,
|
name: true,
|
||||||
objects: true,
|
objectType: true,
|
||||||
emailTemplate: true,
|
objects: true,
|
||||||
emailAccount: true,
|
emailTemplate: true,
|
||||||
recipientEmail: true,
|
emailAccount: true,
|
||||||
recipientType: true,
|
fromEmail: true
|
||||||
recipient: true,
|
}}
|
||||||
fromEmail: true
|
bordered={false}
|
||||||
}}
|
labelWidth={115}
|
||||||
bordered={false}
|
isEditing
|
||||||
labelWidth={115}
|
objectData={objectData}
|
||||||
isEditing
|
/>
|
||||||
objectData={objectData}
|
<Form.Item
|
||||||
/>
|
name='recipients'
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
type: 'array',
|
||||||
|
min: 1,
|
||||||
|
message: 'At least one recipient is required'
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
style={{ marginBottom: 0, marginTop: 16 }}
|
||||||
|
>
|
||||||
|
<ObjectChildCards
|
||||||
|
type='emailMessage'
|
||||||
|
name='recipients'
|
||||||
|
objectData={objectData}
|
||||||
|
isEditing
|
||||||
|
selectedIndex={recipientIndex}
|
||||||
|
onSelectedIndexChange={setRecipientIndex}
|
||||||
|
visibleProperties={{
|
||||||
|
object: true,
|
||||||
|
recipientEmail: true,
|
||||||
|
recipientType: true,
|
||||||
|
recipient: true
|
||||||
|
}}
|
||||||
|
emptyText='No recipients available'
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
submitText='Send'
|
submitText='Send'
|
||||||
title='Send Email'
|
title='Send Email'
|
||||||
formValid={
|
formValid={
|
||||||
formValid && Boolean(objectData?.recipientEmail) && accounts.length > 0
|
formValid &&
|
||||||
|
recipients.some((recipient) => recipient?.recipientEmail) &&
|
||||||
|
accounts.length > 0
|
||||||
}
|
}
|
||||||
loading={submitLoading}
|
loading={submitLoading}
|
||||||
sideBarGrow
|
sideBarGrow
|
||||||
@ -148,7 +202,9 @@ const EmailWizardContent = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TemplatePreview
|
<TemplatePreview
|
||||||
objectData={toTemplateObjectData(objectData?.objects)}
|
objectData={toTemplateObjectData(
|
||||||
|
previewObject ? [previewObject] : []
|
||||||
|
)}
|
||||||
template={objectData?.emailTemplate}
|
template={objectData?.emailTemplate}
|
||||||
templateType='emailTemplate'
|
templateType='emailTemplate'
|
||||||
capabilities={{
|
capabilities={{
|
||||||
@ -198,10 +254,7 @@ const NewEmailMessage = ({
|
|||||||
const fetchedMessageIdRef = useRef(null)
|
const fetchedMessageIdRef = useRef(null)
|
||||||
fetchObjectRef.current = fetchObject
|
fetchObjectRef.current = fetchObject
|
||||||
fetchObjectsRef.current = fetchObjects
|
fetchObjectsRef.current = fetchObjects
|
||||||
const initialValues = useMemo(
|
const initialValues = useMemo(() => ({ ...defaultValues }), [defaultValues])
|
||||||
() => ({ read: false, ...defaultValues }),
|
|
||||||
[defaultValues]
|
|
||||||
)
|
|
||||||
|
|
||||||
const loadAccounts = useCallback(async () => {
|
const loadAccounts = useCallback(async () => {
|
||||||
const result = await fetchObjectsRef.current('emailAccount', {
|
const result = await fetchObjectsRef.current('emailAccount', {
|
||||||
@ -303,7 +356,9 @@ const NewEmailMessage = ({
|
|||||||
getContainer={() => document.body}
|
getContainer={() => document.body}
|
||||||
>
|
>
|
||||||
<ProgressDisplay
|
<ProgressDisplay
|
||||||
percent={progressByState[sendingMessage?.state?.type] || 0}
|
percent={Math.round(
|
||||||
|
(Number(sendingMessage?.state?.progress) || 0) * 100
|
||||||
|
)}
|
||||||
status={
|
status={
|
||||||
sendingMessage?.state?.type === 'error' ? 'exception' : 'active'
|
sendingMessage?.state?.type === 'error' ? 'exception' : 'active'
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,7 +15,7 @@ import MailIcon from '../../Icons/MailIcon'
|
|||||||
import EmailTemplateIcon from '../../Icons/EmailTemplateIcon'
|
import EmailTemplateIcon from '../../Icons/EmailTemplateIcon'
|
||||||
import NewEmailMessage from '../Management/EmailMessages/NewEmailMessage'
|
import NewEmailMessage from '../Management/EmailMessages/NewEmailMessage'
|
||||||
import {
|
import {
|
||||||
getDefaultEmailRecipient,
|
candidatesToRecipients,
|
||||||
resolveEmailRecipients,
|
resolveEmailRecipients,
|
||||||
resolveEmailRecipientsForObjects
|
resolveEmailRecipientsForObjects
|
||||||
} from '../utils/emailRecipients'
|
} from '../utils/emailRecipients'
|
||||||
@ -225,8 +225,8 @@ const EmailSendButton = ({
|
|||||||
]
|
]
|
||||||
}, [handleTemplateSelect, templates])
|
}, [handleTemplateSelect, templates])
|
||||||
|
|
||||||
const defaultRecipient = useMemo(
|
const defaultRecipients = useMemo(
|
||||||
() => getDefaultEmailRecipient(recipients),
|
() => candidatesToRecipients(recipients),
|
||||||
[recipients]
|
[recipients]
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -271,7 +271,7 @@ const EmailSendButton = ({
|
|||||||
objectType: type,
|
objectType: type,
|
||||||
objects: selectedObjects,
|
objects: selectedObjects,
|
||||||
emailTemplate: selectedTemplate,
|
emailTemplate: selectedTemplate,
|
||||||
...defaultRecipient
|
recipients: defaultRecipients
|
||||||
}}
|
}}
|
||||||
recipientCandidates={recipients}
|
recipientCandidates={recipients}
|
||||||
/>
|
/>
|
||||||
|
|||||||
242
src/components/Dashboard/common/ObjectChildCards.jsx
Normal file
242
src/components/Dashboard/common/ObjectChildCards.jsx
Normal file
@ -0,0 +1,242 @@
|
|||||||
|
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import { Button, Card, Flex, Space, Typography } from 'antd'
|
||||||
|
import ObjectInfo from './ObjectInfo'
|
||||||
|
import ChevronLeftIcon from '../../Icons/ChevronLeftIcon'
|
||||||
|
import ChevronRightIcon from '../../Icons/ChevronRightIcon'
|
||||||
|
import { getModelProperty } from '../../../database/ObjectModels'
|
||||||
|
|
||||||
|
const { Text } = Typography
|
||||||
|
|
||||||
|
const resolveChangeValue = (val, type) => {
|
||||||
|
if (type === 'bool') return val
|
||||||
|
if (val?.target && typeof val.target === 'object') {
|
||||||
|
return val.target.value
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
const ObjectChildCards = ({
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
label,
|
||||||
|
properties: propertiesOverride,
|
||||||
|
columns: columnsOverride,
|
||||||
|
visibleProperties = {},
|
||||||
|
objectData = null,
|
||||||
|
parentData = null,
|
||||||
|
value,
|
||||||
|
isEditing = false,
|
||||||
|
onChange,
|
||||||
|
selectedIndex,
|
||||||
|
onSelectedIndexChange,
|
||||||
|
emptyText = 'No items',
|
||||||
|
bordered = false,
|
||||||
|
labelWidth = 115,
|
||||||
|
column = 1,
|
||||||
|
...cardProps
|
||||||
|
}) => {
|
||||||
|
const childProperty = useMemo(() => {
|
||||||
|
if (!type || !name) return null
|
||||||
|
return getModelProperty(type, name)
|
||||||
|
}, [name, type])
|
||||||
|
|
||||||
|
const properties = useMemo(() => {
|
||||||
|
if (Array.isArray(propertiesOverride) && propertiesOverride.length) {
|
||||||
|
return propertiesOverride
|
||||||
|
}
|
||||||
|
return Array.isArray(childProperty?.properties)
|
||||||
|
? childProperty.properties
|
||||||
|
: []
|
||||||
|
}, [childProperty, propertiesOverride])
|
||||||
|
|
||||||
|
const propertyDefinitions = useMemo(() => {
|
||||||
|
const columnNames =
|
||||||
|
Array.isArray(columnsOverride) && columnsOverride.length
|
||||||
|
? columnsOverride
|
||||||
|
: Array.isArray(childProperty?.columns)
|
||||||
|
? childProperty.columns
|
||||||
|
: []
|
||||||
|
|
||||||
|
if (!columnNames.length) return properties
|
||||||
|
|
||||||
|
const propertyMap = new Map(
|
||||||
|
properties.map((property) => [property.name, property])
|
||||||
|
)
|
||||||
|
return columnNames
|
||||||
|
.map((columnName) => propertyMap.get(columnName))
|
||||||
|
.filter(Boolean)
|
||||||
|
}, [childProperty, columnsOverride, properties])
|
||||||
|
|
||||||
|
const items = useMemo(() => {
|
||||||
|
if (Array.isArray(value)) return value
|
||||||
|
if (name && Array.isArray(objectData?.[name])) return objectData[name]
|
||||||
|
return []
|
||||||
|
}, [name, objectData, value])
|
||||||
|
|
||||||
|
const [internalIndex, setInternalIndex] = useState(0)
|
||||||
|
const isControlled = typeof onSelectedIndexChange === 'function'
|
||||||
|
const currentIndex = isControlled ? (selectedIndex ?? 0) : internalIndex
|
||||||
|
|
||||||
|
const setCurrentIndex = useCallback(
|
||||||
|
(nextIndex) => {
|
||||||
|
if (isControlled) {
|
||||||
|
onSelectedIndexChange(nextIndex)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setInternalIndex(nextIndex)
|
||||||
|
},
|
||||||
|
[isControlled, onSelectedIndexChange]
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!items.length) {
|
||||||
|
if (currentIndex !== 0) setCurrentIndex(0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (currentIndex > items.length - 1) {
|
||||||
|
setCurrentIndex(items.length - 1)
|
||||||
|
}
|
||||||
|
if (currentIndex < 0) {
|
||||||
|
setCurrentIndex(0)
|
||||||
|
}
|
||||||
|
}, [currentIndex, items.length, setCurrentIndex])
|
||||||
|
|
||||||
|
const currentRecord = items[currentIndex] || null
|
||||||
|
|
||||||
|
const handleStep = useCallback(
|
||||||
|
(delta) => {
|
||||||
|
const nextIndex = currentIndex + delta
|
||||||
|
if (nextIndex < 0 || nextIndex > items.length - 1) return
|
||||||
|
setCurrentIndex(nextIndex)
|
||||||
|
},
|
||||||
|
[currentIndex, items.length, setCurrentIndex]
|
||||||
|
)
|
||||||
|
|
||||||
|
const propertyMap = useMemo(() => {
|
||||||
|
const map = new Map()
|
||||||
|
properties.forEach((property) => {
|
||||||
|
if (property?.name) map.set(property.name, property)
|
||||||
|
})
|
||||||
|
return map
|
||||||
|
}, [properties])
|
||||||
|
|
||||||
|
const handlePropertyChange = useCallback(
|
||||||
|
(propName, newVal) => {
|
||||||
|
const property = propertyMap.get(propName)
|
||||||
|
const resolved = resolveChangeValue(newVal, property?.type)
|
||||||
|
const parent = parentData || objectData
|
||||||
|
const nextItems = items.map((item, index) => {
|
||||||
|
if (index !== currentIndex) return item
|
||||||
|
const updated = { ...item, [propName]: resolved }
|
||||||
|
properties.forEach((childProperty) => {
|
||||||
|
if (
|
||||||
|
childProperty?.name === propName ||
|
||||||
|
typeof childProperty?.value !== 'function'
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const computed = childProperty.value(updated, parent)
|
||||||
|
if (computed !== undefined) {
|
||||||
|
updated[childProperty.name] = computed
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return updated
|
||||||
|
})
|
||||||
|
onChange?.(nextItems)
|
||||||
|
},
|
||||||
|
[
|
||||||
|
currentIndex,
|
||||||
|
items,
|
||||||
|
objectData,
|
||||||
|
onChange,
|
||||||
|
parentData,
|
||||||
|
properties,
|
||||||
|
propertyMap
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
const title = label || childProperty?.label || name || 'Items'
|
||||||
|
const countLabel = items.length
|
||||||
|
? `${currentIndex + 1}/${items.length}`
|
||||||
|
: '0/0'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
size='small'
|
||||||
|
title={
|
||||||
|
<Flex align='center' justify='space-between' gap='small'>
|
||||||
|
<Text>
|
||||||
|
{title} ({countLabel})
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
}
|
||||||
|
extra={
|
||||||
|
<Space.Compact size='small' style={{ height: '22px' }}>
|
||||||
|
<Button
|
||||||
|
type='text'
|
||||||
|
icon={
|
||||||
|
<ChevronLeftIcon
|
||||||
|
style={{ fontSize: '10px', marginBottom: '6px' }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onClick={() => handleStep(-1)}
|
||||||
|
style={{ padding: 0, height: '22px', width: '32px' }}
|
||||||
|
disabled={!items.length || currentIndex <= 0}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type='text'
|
||||||
|
icon={
|
||||||
|
<ChevronRightIcon
|
||||||
|
style={{ fontSize: '10px', marginBottom: '6px' }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onClick={() => handleStep(1)}
|
||||||
|
style={{ padding: 0, height: '22px', width: '32px' }}
|
||||||
|
disabled={!items.length || currentIndex >= items.length - 1}
|
||||||
|
/>
|
||||||
|
</Space.Compact>
|
||||||
|
}
|
||||||
|
styles={{ header: { padding: '0px 8px 0px 12px' } }}
|
||||||
|
{...cardProps}
|
||||||
|
>
|
||||||
|
{currentRecord ? (
|
||||||
|
<ObjectInfo
|
||||||
|
propertyDefinitions={propertyDefinitions}
|
||||||
|
objectData={currentRecord}
|
||||||
|
parentData={parentData || objectData}
|
||||||
|
isEditing={isEditing}
|
||||||
|
onPropertyChange={isEditing ? handlePropertyChange : undefined}
|
||||||
|
visibleProperties={visibleProperties}
|
||||||
|
column={column}
|
||||||
|
bordered={bordered}
|
||||||
|
labelWidth={labelWidth}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Text type='secondary'>{emptyText}</Text>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ObjectChildCards.propTypes = {
|
||||||
|
name: PropTypes.string,
|
||||||
|
type: PropTypes.string,
|
||||||
|
label: PropTypes.string,
|
||||||
|
properties: PropTypes.array,
|
||||||
|
columns: PropTypes.array,
|
||||||
|
visibleProperties: PropTypes.object,
|
||||||
|
objectData: PropTypes.object,
|
||||||
|
parentData: PropTypes.object,
|
||||||
|
value: PropTypes.array,
|
||||||
|
isEditing: PropTypes.bool,
|
||||||
|
onChange: PropTypes.func,
|
||||||
|
selectedIndex: PropTypes.number,
|
||||||
|
onSelectedIndexChange: PropTypes.func,
|
||||||
|
emptyText: PropTypes.string,
|
||||||
|
bordered: PropTypes.bool,
|
||||||
|
labelWidth: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||||
|
column: PropTypes.oneOfType([PropTypes.number, PropTypes.object])
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ObjectChildCards
|
||||||
@ -5,7 +5,7 @@ const normalizeType = (type) =>
|
|||||||
? `${type.charAt(0).toLowerCase()}${type.slice(1)}`
|
? `${type.charAt(0).toLowerCase()}${type.slice(1)}`
|
||||||
: type
|
: type
|
||||||
|
|
||||||
const getReferenceId = (value) => {
|
export const getReferenceId = (value) => {
|
||||||
if (typeof value === 'string') return value
|
if (typeof value === 'string') return value
|
||||||
return value?._id?._id || value?._id || null
|
return value?._id?._id || value?._id || null
|
||||||
}
|
}
|
||||||
@ -14,7 +14,7 @@ const addCandidate = (candidates, candidate) => {
|
|||||||
if (!candidate?.email) return
|
if (!candidate?.email) return
|
||||||
const key = `${candidate.email.toLowerCase()}:${candidate.recipientType || ''}:${
|
const key = `${candidate.email.toLowerCase()}:${candidate.recipientType || ''}:${
|
||||||
getReferenceId(candidate.recipient) || ''
|
getReferenceId(candidate.recipient) || ''
|
||||||
}`
|
}:${getReferenceId(candidate.object) || ''}`
|
||||||
if (!candidates.some((entry) => entry.key === key)) {
|
if (!candidates.some((entry) => entry.key === key)) {
|
||||||
candidates.push({ ...candidate, key })
|
candidates.push({ ...candidate, key })
|
||||||
}
|
}
|
||||||
@ -40,7 +40,8 @@ export const resolveEmailRecipients = async ({
|
|||||||
addCandidate(candidates, {
|
addCandidate(candidates, {
|
||||||
email: value.trim(),
|
email: value.trim(),
|
||||||
label: `${property.label || property.name}: ${value.trim()}`,
|
label: `${property.label || property.name}: ${value.trim()}`,
|
||||||
property: property.name
|
property: property.name,
|
||||||
|
object: objectData
|
||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -71,7 +72,8 @@ export const resolveEmailRecipients = async ({
|
|||||||
}`,
|
}`,
|
||||||
property: property.name,
|
property: property.name,
|
||||||
recipientType: resolvedType,
|
recipientType: resolvedType,
|
||||||
recipient
|
recipient,
|
||||||
|
object: objectData
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -79,6 +81,15 @@ export const resolveEmailRecipients = async ({
|
|||||||
return candidates
|
return candidates
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hydrateObject = async (item, type, fetchObject) => {
|
||||||
|
if (item && typeof item === 'object' && !Array.isArray(item)) {
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
const id = getReferenceId(item)
|
||||||
|
if (!id || !type || typeof fetchObject !== 'function') return null
|
||||||
|
return fetchObject(id, type)
|
||||||
|
}
|
||||||
|
|
||||||
export const resolveEmailRecipientsForObjects = async ({
|
export const resolveEmailRecipientsForObjects = async ({
|
||||||
type,
|
type,
|
||||||
objects,
|
objects,
|
||||||
@ -86,40 +97,53 @@ export const resolveEmailRecipientsForObjects = async ({
|
|||||||
}) => {
|
}) => {
|
||||||
const list = Array.isArray(objects) ? objects.filter(Boolean) : []
|
const list = Array.isArray(objects) ? objects.filter(Boolean) : []
|
||||||
const merged = []
|
const merged = []
|
||||||
for (const objectData of list) {
|
for (const item of list) {
|
||||||
|
const objectData = await hydrateObject(item, type, fetchObject)
|
||||||
|
if (!objectData) continue
|
||||||
const result = await resolveEmailRecipients({
|
const result = await resolveEmailRecipients({
|
||||||
type,
|
type,
|
||||||
objectData,
|
objectData,
|
||||||
fetchObject
|
fetchObject
|
||||||
})
|
})
|
||||||
for (const candidate of result) {
|
if (result.length) {
|
||||||
addCandidate(merged, candidate)
|
for (const candidate of result) {
|
||||||
|
addCandidate(merged, candidate)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
merged.push({
|
||||||
|
email: '',
|
||||||
|
object: objectData,
|
||||||
|
key: `:${getReferenceId(objectData) || ''}`
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return merged
|
return merged
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const candidatesToRecipients = (candidates = []) => {
|
||||||
|
if (!Array.isArray(candidates) || !candidates.length) return []
|
||||||
|
|
||||||
|
return candidates.map((candidate) => ({
|
||||||
|
recipientEmail: candidate.email || '',
|
||||||
|
recipientType: candidate.recipientType ?? null,
|
||||||
|
recipient: candidate.recipient ?? null,
|
||||||
|
object: candidate.object ?? null,
|
||||||
|
read: false,
|
||||||
|
sent: false
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
/** Prefer a client/vendor ref; otherwise use a plain email candidate. */
|
/** Prefer a client/vendor ref; otherwise use a plain email candidate. */
|
||||||
export const getDefaultEmailRecipient = (candidates = []) => {
|
export const getDefaultEmailRecipient = (candidates = []) => {
|
||||||
if (!Array.isArray(candidates) || !candidates.length) return null
|
const recipients = candidatesToRecipients(candidates)
|
||||||
|
if (!recipients.length) return null
|
||||||
|
|
||||||
const objectCandidate = candidates.find(
|
const objectRecipient = recipients.find(
|
||||||
(candidate) =>
|
(recipient) =>
|
||||||
(candidate.recipientType === 'client' ||
|
(recipient.recipientType === 'client' ||
|
||||||
candidate.recipientType === 'vendor') &&
|
recipient.recipientType === 'vendor') &&
|
||||||
candidate.recipient &&
|
recipient.recipient &&
|
||||||
candidate.email
|
recipient.recipientEmail
|
||||||
)
|
)
|
||||||
if (objectCandidate) {
|
return objectRecipient || recipients[0]
|
||||||
return {
|
|
||||||
recipientEmail: objectCandidate.email,
|
|
||||||
recipientType: objectCandidate.recipientType,
|
|
||||||
recipient: objectCandidate.recipient
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const emailCandidate = candidates.find((candidate) => candidate.email)
|
|
||||||
if (!emailCandidate) return null
|
|
||||||
|
|
||||||
return { recipientEmail: emailCandidate.email }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,11 +11,9 @@ export function resolveObjectsList(objects, objectData) {
|
|||||||
export function toTemplateObjectData(objects) {
|
export function toTemplateObjectData(objects) {
|
||||||
const list = Array.isArray(objects) ? objects.filter(Boolean) : []
|
const list = Array.isArray(objects) ? objects.filter(Boolean) : []
|
||||||
if (!list.length) return {}
|
if (!list.length) return {}
|
||||||
if (list.length === 1) {
|
const first = list[0]
|
||||||
const first = list[0]
|
if (first && typeof first === 'object' && !Array.isArray(first)) {
|
||||||
if (first && typeof first === 'object' && !Array.isArray(first)) {
|
return { ...first, objects: list }
|
||||||
return first
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return { objects: list }
|
return { objects: list }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,7 +15,28 @@ const NewEmailMessage = lazy(
|
|||||||
import('../../components/Dashboard/Management/EmailMessages/NewEmailMessage')
|
import('../../components/Dashboard/Management/EmailMessages/NewEmailMessage')
|
||||||
)
|
)
|
||||||
|
|
||||||
const readOnlyAfterCreate = (data) => data?._id != null
|
const readOnlyAfterCreate = (data, parentData) =>
|
||||||
|
(parentData?._id ?? data?._id) != null
|
||||||
|
|
||||||
|
const getReferenceId = (value) => {
|
||||||
|
if (typeof value === 'string') return value
|
||||||
|
return value?._id?._id || value?._id || null
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchRecipientCandidate = (data, parentData) => {
|
||||||
|
if (
|
||||||
|
!Array.isArray(parentData?._recipientCandidates) ||
|
||||||
|
!parentData._recipientCandidates.length
|
||||||
|
) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
const objectId = getReferenceId(data?.object)
|
||||||
|
return parentData._recipientCandidates.find((candidate) => {
|
||||||
|
if (candidate.email !== data?.recipientEmail) return false
|
||||||
|
if (!objectId) return true
|
||||||
|
return getReferenceId(candidate.object) === objectId
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export const EmailMessage = {
|
export const EmailMessage = {
|
||||||
name: 'emailMessage',
|
name: 'emailMessage',
|
||||||
@ -74,49 +95,38 @@ export const EmailMessage = {
|
|||||||
'_reference',
|
'_reference',
|
||||||
'name',
|
'name',
|
||||||
'state',
|
'state',
|
||||||
'recipientEmail',
|
|
||||||
'fromEmail',
|
'fromEmail',
|
||||||
'messageId',
|
|
||||||
'emailTemplate',
|
'emailTemplate',
|
||||||
'emailAccount',
|
'emailAccount',
|
||||||
'objectType',
|
'objectType',
|
||||||
'sentAt',
|
'sentAt',
|
||||||
'readAt',
|
'firstReadAt',
|
||||||
|
'lastReadAt',
|
||||||
'createdAt',
|
'createdAt',
|
||||||
'updatedAt'
|
'updatedAt'
|
||||||
],
|
],
|
||||||
filters: [
|
filters: [
|
||||||
'name',
|
'name',
|
||||||
'state',
|
'state',
|
||||||
'recipientEmail',
|
|
||||||
'fromEmail',
|
'fromEmail',
|
||||||
'messageId',
|
|
||||||
'emailTemplate',
|
'emailTemplate',
|
||||||
'emailAccount',
|
'emailAccount',
|
||||||
'objectType',
|
'objectType',
|
||||||
'recipientType',
|
|
||||||
'read',
|
|
||||||
'sentAt',
|
'sentAt',
|
||||||
'readAt',
|
'firstReadAt',
|
||||||
|
'lastReadAt',
|
||||||
'createdAt',
|
'createdAt',
|
||||||
'updatedAt',
|
'updatedAt',
|
||||||
'_reference'
|
'_reference'
|
||||||
],
|
],
|
||||||
group: [
|
group: ['state', 'emailTemplate', 'emailAccount', 'objectType'],
|
||||||
'state',
|
|
||||||
'emailTemplate',
|
|
||||||
'emailAccount',
|
|
||||||
'objectType',
|
|
||||||
'recipientType'
|
|
||||||
],
|
|
||||||
sorters: [
|
sorters: [
|
||||||
'name',
|
'name',
|
||||||
'state',
|
'state',
|
||||||
'recipientEmail',
|
|
||||||
'fromEmail',
|
'fromEmail',
|
||||||
'messageId',
|
|
||||||
'sentAt',
|
'sentAt',
|
||||||
'readAt',
|
'firstReadAt',
|
||||||
|
'lastReadAt',
|
||||||
'createdAt',
|
'createdAt',
|
||||||
'updatedAt'
|
'updatedAt'
|
||||||
],
|
],
|
||||||
@ -171,6 +181,18 @@ export const EmailMessage = {
|
|||||||
label: 'Sent At',
|
label: 'Sent At',
|
||||||
type: 'dateTime',
|
type: 'dateTime',
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
|
value: (data) => {
|
||||||
|
const recipients = Array.isArray(data?.recipients)
|
||||||
|
? data.recipients
|
||||||
|
: []
|
||||||
|
return recipients.reduce((latest, recipient) => {
|
||||||
|
if (!recipient?.sent || recipient.sentAt == null) return latest
|
||||||
|
if (latest == null) return recipient.sentAt
|
||||||
|
return new Date(recipient.sentAt) > new Date(latest)
|
||||||
|
? recipient.sentAt
|
||||||
|
: latest
|
||||||
|
}, undefined)
|
||||||
|
},
|
||||||
columnWidth: 200
|
columnWidth: 200
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -182,19 +204,24 @@ export const EmailMessage = {
|
|||||||
columnWidth: 240
|
columnWidth: 240
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'readAt',
|
name: 'firstReadAt',
|
||||||
label: 'Read At',
|
label: 'First Read At',
|
||||||
type: 'dateTime',
|
type: 'dateTime',
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
|
value: (data) => {
|
||||||
|
const recipients = Array.isArray(data?.recipients)
|
||||||
|
? data.recipients
|
||||||
|
: []
|
||||||
|
return recipients.reduce((earliest, recipient) => {
|
||||||
|
if (recipient?.readAt == null) return earliest
|
||||||
|
if (earliest == null) return recipient.readAt
|
||||||
|
return new Date(recipient.readAt) < new Date(earliest)
|
||||||
|
? recipient.readAt
|
||||||
|
: earliest
|
||||||
|
}, undefined)
|
||||||
|
},
|
||||||
columnWidth: 200
|
columnWidth: 200
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'read',
|
|
||||||
label: 'Read',
|
|
||||||
type: 'bool',
|
|
||||||
readOnly: true,
|
|
||||||
columnWidth: 100
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'emailTemplate',
|
name: 'emailTemplate',
|
||||||
label: 'Template',
|
label: 'Template',
|
||||||
@ -210,7 +237,178 @@ export const EmailMessage = {
|
|||||||
}),
|
}),
|
||||||
columnWidth: 200
|
columnWidth: 200
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'lastReadAt',
|
||||||
|
label: 'Last Read At',
|
||||||
|
type: 'dateTime',
|
||||||
|
readOnly: true,
|
||||||
|
value: (data) => {
|
||||||
|
const recipients = Array.isArray(data?.recipients)
|
||||||
|
? data.recipients
|
||||||
|
: []
|
||||||
|
return recipients.reduce((latest, recipient) => {
|
||||||
|
if (recipient?.readAt == null) return latest
|
||||||
|
if (latest == null) return recipient.readAt
|
||||||
|
return new Date(recipient.readAt) > new Date(latest)
|
||||||
|
? recipient.readAt
|
||||||
|
: latest
|
||||||
|
}, undefined)
|
||||||
|
},
|
||||||
|
columnWidth: 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'emailAccount',
|
||||||
|
label: 'Email Account',
|
||||||
|
type: 'object',
|
||||||
|
objectType: 'emailAccount',
|
||||||
|
required: true,
|
||||||
|
readOnly: readOnlyAfterCreate,
|
||||||
|
showHyperlink: true,
|
||||||
|
masterFilter: { active: true },
|
||||||
|
columnWidth: 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'recipients',
|
||||||
|
label: 'Recipients',
|
||||||
|
type: 'objectChildren',
|
||||||
|
required: true,
|
||||||
|
canAddRemove: true,
|
||||||
|
size: 'medium',
|
||||||
|
hiddenPropertyWidth: 160,
|
||||||
|
columns: [
|
||||||
|
'object',
|
||||||
|
'recipientEmail',
|
||||||
|
'recipientType',
|
||||||
|
'recipient',
|
||||||
|
'messageId',
|
||||||
|
'attachments',
|
||||||
|
'read',
|
||||||
|
'sent',
|
||||||
|
'readAt'
|
||||||
|
],
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
name: 'object',
|
||||||
|
label: 'Object',
|
||||||
|
type: 'object',
|
||||||
|
objectType: (_data, parentData) => parentData?.objectType,
|
||||||
|
readOnly: true,
|
||||||
|
showHyperlink: true,
|
||||||
|
columnWidth: 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'recipientEmail',
|
||||||
|
label: 'Recipient Email',
|
||||||
|
type: 'email',
|
||||||
|
required: true,
|
||||||
|
readOnly: readOnlyAfterCreate,
|
||||||
|
columnWidth: 260
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'recipientType',
|
||||||
|
label: 'Recipient Type',
|
||||||
|
type: 'objectType',
|
||||||
|
readOnly: readOnlyAfterCreate,
|
||||||
|
masterFilter: ['client', 'vendor'],
|
||||||
|
value: (data, parentData) => {
|
||||||
|
const match = matchRecipientCandidate(data, parentData)
|
||||||
|
if (!match) return undefined
|
||||||
|
return match.recipientType ?? null
|
||||||
|
},
|
||||||
|
columnWidth: 160
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'recipient',
|
||||||
|
label: 'Recipient',
|
||||||
|
type: 'object',
|
||||||
|
objectType: (data) => data?.recipientType,
|
||||||
|
readOnly: readOnlyAfterCreate,
|
||||||
|
showHyperlink: true,
|
||||||
|
value: (data, parentData) => {
|
||||||
|
const match = matchRecipientCandidate(data, parentData)
|
||||||
|
if (!match) return undefined
|
||||||
|
return match.recipient ?? null
|
||||||
|
},
|
||||||
|
columnWidth: 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'messageId',
|
||||||
|
label: 'Message ID',
|
||||||
|
type: 'miscId',
|
||||||
|
readOnly: true,
|
||||||
|
showCopy: true,
|
||||||
|
columnWidth: 280
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'pixelId',
|
||||||
|
label: 'Pixel ID',
|
||||||
|
type: 'miscId',
|
||||||
|
readOnly: true,
|
||||||
|
showCopy: true,
|
||||||
|
columnWidth: 280
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'attachments',
|
||||||
|
label: 'Attachments',
|
||||||
|
type: 'objectList',
|
||||||
|
objectType: 'file',
|
||||||
|
readOnly: true,
|
||||||
|
showHyperlink: true,
|
||||||
|
columnWidth: 230
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'read',
|
||||||
|
label: 'Read',
|
||||||
|
type: 'bool',
|
||||||
|
readOnly: true,
|
||||||
|
columnWidth: 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'sent',
|
||||||
|
label: 'Sent',
|
||||||
|
type: 'bool',
|
||||||
|
readOnly: true,
|
||||||
|
columnWidth: 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'readAt',
|
||||||
|
label: 'Read At',
|
||||||
|
type: 'dateTime',
|
||||||
|
readOnly: true,
|
||||||
|
columnWidth: 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'sentAt',
|
||||||
|
label: 'Sent At',
|
||||||
|
type: 'dateTime',
|
||||||
|
readOnly: true,
|
||||||
|
columnWidth: 200
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'fromEmail',
|
||||||
|
label: 'From Email',
|
||||||
|
type: 'email',
|
||||||
|
required: true,
|
||||||
|
readOnly: true,
|
||||||
|
value: (data) => data?.emailAccount?.fromEmail,
|
||||||
|
columnWidth: 260
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'subject',
|
||||||
|
label: 'Rendered Subject',
|
||||||
|
type: 'text',
|
||||||
|
readOnly: true,
|
||||||
|
columnWidth: 300
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'content',
|
||||||
|
label: 'Rendered Body',
|
||||||
|
type: 'codeBlock',
|
||||||
|
language: 'html',
|
||||||
|
readOnly: true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'objectType',
|
name: 'objectType',
|
||||||
label: 'Object Type',
|
label: 'Object Type',
|
||||||
@ -228,115 +426,6 @@ export const EmailMessage = {
|
|||||||
readOnly: readOnlyAfterCreate,
|
readOnly: readOnlyAfterCreate,
|
||||||
showHyperlink: true,
|
showHyperlink: true,
|
||||||
columnWidth: 200
|
columnWidth: 200
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'emailAccount',
|
|
||||||
label: 'Email Account',
|
|
||||||
type: 'object',
|
|
||||||
objectType: 'emailAccount',
|
|
||||||
required: true,
|
|
||||||
readOnly: readOnlyAfterCreate,
|
|
||||||
showHyperlink: true,
|
|
||||||
masterFilter: { active: true },
|
|
||||||
columnWidth: 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'recipientEmail',
|
|
||||||
label: 'Recipient Email',
|
|
||||||
type: 'email',
|
|
||||||
required: true,
|
|
||||||
readOnly: readOnlyAfterCreate,
|
|
||||||
options: (data) =>
|
|
||||||
Array.isArray(data?._recipientCandidates) &&
|
|
||||||
data._recipientCandidates.length
|
|
||||||
? data._recipientCandidates.map((candidate) => ({
|
|
||||||
value: candidate.email,
|
|
||||||
label: candidate.label
|
|
||||||
}))
|
|
||||||
: undefined,
|
|
||||||
columnWidth: 260
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'recipientType',
|
|
||||||
label: 'Recipient Type',
|
|
||||||
type: 'objectType',
|
|
||||||
readOnly: readOnlyAfterCreate,
|
|
||||||
value: (data) => {
|
|
||||||
if (
|
|
||||||
!Array.isArray(data?._recipientCandidates) ||
|
|
||||||
!data._recipientCandidates.length
|
|
||||||
) {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
const match = data._recipientCandidates.find(
|
|
||||||
(candidate) => candidate.email === data?.recipientEmail
|
|
||||||
)
|
|
||||||
if (!match) return undefined
|
|
||||||
return match.recipientType ?? null
|
|
||||||
},
|
|
||||||
columnWidth: 160
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'recipient',
|
|
||||||
label: 'Recipient',
|
|
||||||
type: 'object',
|
|
||||||
objectType: (data) => data?.recipientType,
|
|
||||||
readOnly: readOnlyAfterCreate,
|
|
||||||
showHyperlink: true,
|
|
||||||
value: (data) => {
|
|
||||||
if (
|
|
||||||
!Array.isArray(data?._recipientCandidates) ||
|
|
||||||
!data._recipientCandidates.length
|
|
||||||
) {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
const match = data._recipientCandidates.find(
|
|
||||||
(candidate) => candidate.email === data?.recipientEmail
|
|
||||||
)
|
|
||||||
if (!match) return undefined
|
|
||||||
return match.recipient ?? null
|
|
||||||
},
|
|
||||||
columnWidth: 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'fromEmail',
|
|
||||||
label: 'From Email',
|
|
||||||
type: 'email',
|
|
||||||
required: true,
|
|
||||||
readOnly: true,
|
|
||||||
value: (data) => data?.emailAccount?.fromEmail,
|
|
||||||
columnWidth: 260
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'messageId',
|
|
||||||
label: 'Message ID',
|
|
||||||
type: 'text',
|
|
||||||
readOnly: true,
|
|
||||||
showCopy: true,
|
|
||||||
columnWidth: 280
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'subject',
|
|
||||||
label: 'Rendered Subject',
|
|
||||||
type: 'text',
|
|
||||||
readOnly: true,
|
|
||||||
columnWidth: 300
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'content',
|
|
||||||
label: 'Rendered Body',
|
|
||||||
type: 'codeBlock',
|
|
||||||
language: 'html',
|
|
||||||
readOnly: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'attachments',
|
|
||||||
label: 'Attachments',
|
|
||||||
type: 'objectList',
|
|
||||||
objectType: 'file',
|
|
||||||
readOnly: true,
|
|
||||||
showHyperlink: true,
|
|
||||||
columnWidth: 230
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user