Compare commits
No commits in common. "968dc0667a79c9274c08259c512fe7c5ff8853d8" and "eea7e1f724f5abfbb6403454e38866fe9fcf531e" have entirely different histories.
968dc0667a
...
eea7e1f724
@ -20,8 +20,7 @@ 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'
|
||||||
|
|
||||||
@ -141,7 +140,6 @@ 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({
|
||||||
@ -151,8 +149,6 @@ 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)
|
||||||
|
|||||||
@ -3,11 +3,8 @@
|
|||||||
* Mirrors custom elements / style attrs handled in farmcontrol-ws TemplateManager.
|
* Mirrors custom elements / style attrs handled in farmcontrol-ws TemplateManager.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { syntaxTree } from '@codemirror/language'
|
|
||||||
|
|
||||||
const styleAttrs = [
|
const styleAttrs = [
|
||||||
'padding',
|
'padding',
|
||||||
'margin',
|
|
||||||
'width',
|
'width',
|
||||||
'height',
|
'height',
|
||||||
'minWidth',
|
'minWidth',
|
||||||
@ -20,8 +17,6 @@ const styleAttrs = [
|
|||||||
'border',
|
'border',
|
||||||
'borderRadius',
|
'borderRadius',
|
||||||
'vertical',
|
'vertical',
|
||||||
'header',
|
|
||||||
'footer',
|
|
||||||
'grow',
|
'grow',
|
||||||
'shrink',
|
'shrink',
|
||||||
'color',
|
'color',
|
||||||
@ -53,9 +48,7 @@ const alignValues = [
|
|||||||
export const fcTemplateAttributes = [
|
export const fcTemplateAttributes = [
|
||||||
...styleAttrs.map((name) => {
|
...styleAttrs.map((name) => {
|
||||||
const attr = { name, global: true }
|
const attr = { name, global: true }
|
||||||
if (name === 'vertical' || name === 'header' || name === 'footer') {
|
if (name === 'vertical') attr.values = ['true', 'false']
|
||||||
attr.values = ['true', 'false']
|
|
||||||
}
|
|
||||||
if (name === 'justify') attr.values = justifyValues
|
if (name === 'justify') attr.values = justifyValues
|
||||||
if (name === 'align') attr.values = alignValues
|
if (name === 'align') attr.values = alignValues
|
||||||
if (name === 'textAlign') {
|
if (name === 'textAlign') {
|
||||||
@ -148,69 +141,6 @@ export const fcEmailTemplateHelpers = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Pagination placeholders replaced after layout (`!pageNumber`, `!totalPages`). */
|
|
||||||
export const fcTemplatePlaceholders = [
|
|
||||||
{
|
|
||||||
name: '!pageNumber',
|
|
||||||
detail: 'Current page number'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '!totalPages',
|
|
||||||
detail: 'Total page count'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
export function placeholderCompletionSource() {
|
|
||||||
const options = fcTemplatePlaceholders.map((placeholder) => ({
|
|
||||||
label: placeholder.name,
|
|
||||||
apply: placeholder.name,
|
|
||||||
type: 'variable',
|
|
||||||
detail: placeholder.detail
|
|
||||||
}))
|
|
||||||
|
|
||||||
return (context) => {
|
|
||||||
if (isPlaceholderBlockedContext(context.state, context.pos)) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const before = context.state.sliceDoc(Math.max(0, context.pos - 48), context.pos)
|
|
||||||
const match = before.match(/![a-zA-Z]*$/)
|
|
||||||
if (!match) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
from: context.pos - match[0].length,
|
|
||||||
options
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isPlaceholderBlockedContext(state, position) {
|
|
||||||
let node = syntaxTree(state).resolveInner(position, -1)
|
|
||||||
|
|
||||||
while (node) {
|
|
||||||
if (
|
|
||||||
node.name === 'AttributeValue' ||
|
|
||||||
node.name === 'AttributeName' ||
|
|
||||||
node.name === 'Attribute' ||
|
|
||||||
node.name === 'OpenTag' ||
|
|
||||||
node.name === 'CloseTag' ||
|
|
||||||
node.name === 'StartTag' ||
|
|
||||||
node.name === 'TagName' ||
|
|
||||||
node.name === 'JavascriptExpression' ||
|
|
||||||
node.name === 'Scriptlet' ||
|
|
||||||
node.name === 'Output' ||
|
|
||||||
node.name === 'Comment'
|
|
||||||
) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
node = node.parent
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
export const fcTemplateHelpers = fcDocumentTemplateHelpers
|
export const fcTemplateHelpers = fcDocumentTemplateHelpers
|
||||||
|
|
||||||
export function getFcTemplateHelpers(templateType) {
|
export function getFcTemplateHelpers(templateType) {
|
||||||
|
|||||||
@ -8,10 +8,8 @@ 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'
|
||||||
@ -23,7 +21,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, getModelProperty } from '../../../../database/ObjectModels.js'
|
import { getModelByName } 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'
|
||||||
|
|
||||||
@ -40,7 +38,6 @@ const EmailMessageInfo = () => {
|
|||||||
'EmailMessageInfo',
|
'EmailMessageInfo',
|
||||||
{
|
{
|
||||||
info: true,
|
info: true,
|
||||||
recipients: true,
|
|
||||||
notes: true,
|
notes: true,
|
||||||
auditLogs: false
|
auditLogs: false
|
||||||
}
|
}
|
||||||
@ -90,7 +87,6 @@ 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' }
|
||||||
]}
|
]}
|
||||||
@ -145,28 +141,10 @@ 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,21 +7,23 @@ import {
|
|||||||
useRef,
|
useRef,
|
||||||
useState
|
useState
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import { Form, Modal } from 'antd'
|
import { 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 {
|
import { resolveEmailRecipientsForObjects } from '../../utils/emailRecipients'
|
||||||
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 = {}
|
||||||
|
|
||||||
@ -38,49 +40,20 @@ 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(() => {
|
||||||
const objects = relatedObjectsRef.current
|
if (defaultCandidates.length) {
|
||||||
if (!objects.length) {
|
setCandidates(defaultCandidates)
|
||||||
setCandidates([])
|
return
|
||||||
return undefined
|
|
||||||
}
|
}
|
||||||
let cancelled = false
|
let cancelled = false
|
||||||
resolveEmailRecipientsForObjects({
|
resolveEmailRecipientsForObjects({
|
||||||
type: objectData?.objectType,
|
type: objectData?.objectType,
|
||||||
objects,
|
objects: Array.isArray(objectData?.objects)
|
||||||
|
? objectData.objects
|
||||||
|
: objectData?.object
|
||||||
|
? [objectData.object]
|
||||||
|
: [],
|
||||||
fetchObject
|
fetchObject
|
||||||
}).then((result) => {
|
}).then((result) => {
|
||||||
if (!cancelled) setCandidates(result)
|
if (!cancelled) setCandidates(result)
|
||||||
@ -88,7 +61,13 @@ 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) => ({
|
||||||
@ -97,23 +76,6 @@ 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 =
|
||||||
@ -129,6 +91,18 @@ 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={[
|
||||||
@ -136,60 +110,32 @@ const EmailWizardContent = ({
|
|||||||
title: 'Required',
|
title: 'Required',
|
||||||
key: 'required',
|
key: 'required',
|
||||||
content: (
|
content: (
|
||||||
<div>
|
<ObjectInfo
|
||||||
<ObjectInfo
|
type='emailMessage'
|
||||||
type='emailMessage'
|
column={1}
|
||||||
column={1}
|
visibleProperties={{
|
||||||
visibleProperties={{
|
name: true,
|
||||||
name: true,
|
objectType: true,
|
||||||
objectType: true,
|
objects: true,
|
||||||
objects: true,
|
emailTemplate: true,
|
||||||
emailTemplate: true,
|
emailAccount: true,
|
||||||
emailAccount: true,
|
recipientEmail: true,
|
||||||
fromEmail: true
|
recipientType: true,
|
||||||
}}
|
recipient: true,
|
||||||
bordered={false}
|
fromEmail: true
|
||||||
labelWidth={115}
|
}}
|
||||||
isEditing
|
bordered={false}
|
||||||
objectData={objectData}
|
labelWidth={115}
|
||||||
/>
|
isEditing
|
||||||
<Form.Item
|
objectData={objectData}
|
||||||
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 &&
|
formValid && Boolean(objectData?.recipientEmail) && accounts.length > 0
|
||||||
recipients.some((recipient) => recipient?.recipientEmail) &&
|
|
||||||
accounts.length > 0
|
|
||||||
}
|
}
|
||||||
loading={submitLoading}
|
loading={submitLoading}
|
||||||
sideBarGrow
|
sideBarGrow
|
||||||
@ -202,9 +148,7 @@ const EmailWizardContent = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TemplatePreview
|
<TemplatePreview
|
||||||
objectData={toTemplateObjectData(
|
objectData={toTemplateObjectData(objectData?.objects)}
|
||||||
previewObject ? [previewObject] : []
|
|
||||||
)}
|
|
||||||
template={objectData?.emailTemplate}
|
template={objectData?.emailTemplate}
|
||||||
templateType='emailTemplate'
|
templateType='emailTemplate'
|
||||||
capabilities={{
|
capabilities={{
|
||||||
@ -254,7 +198,10 @@ 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(() => ({ ...defaultValues }), [defaultValues])
|
const initialValues = useMemo(
|
||||||
|
() => ({ read: false, ...defaultValues }),
|
||||||
|
[defaultValues]
|
||||||
|
)
|
||||||
|
|
||||||
const loadAccounts = useCallback(async () => {
|
const loadAccounts = useCallback(async () => {
|
||||||
const result = await fetchObjectsRef.current('emailAccount', {
|
const result = await fetchObjectsRef.current('emailAccount', {
|
||||||
@ -356,9 +303,7 @@ const NewEmailMessage = ({
|
|||||||
getContainer={() => document.body}
|
getContainer={() => document.body}
|
||||||
>
|
>
|
||||||
<ProgressDisplay
|
<ProgressDisplay
|
||||||
percent={Math.round(
|
percent={progressByState[sendingMessage?.state?.type] || 0}
|
||||||
(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 {
|
||||||
candidatesToRecipients,
|
getDefaultEmailRecipient,
|
||||||
resolveEmailRecipients,
|
resolveEmailRecipients,
|
||||||
resolveEmailRecipientsForObjects
|
resolveEmailRecipientsForObjects
|
||||||
} from '../utils/emailRecipients'
|
} from '../utils/emailRecipients'
|
||||||
@ -225,8 +225,8 @@ const EmailSendButton = ({
|
|||||||
]
|
]
|
||||||
}, [handleTemplateSelect, templates])
|
}, [handleTemplateSelect, templates])
|
||||||
|
|
||||||
const defaultRecipients = useMemo(
|
const defaultRecipient = useMemo(
|
||||||
() => candidatesToRecipients(recipients),
|
() => getDefaultEmailRecipient(recipients),
|
||||||
[recipients]
|
[recipients]
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -271,7 +271,7 @@ const EmailSendButton = ({
|
|||||||
objectType: type,
|
objectType: type,
|
||||||
objects: selectedObjects,
|
objects: selectedObjects,
|
||||||
emailTemplate: selectedTemplate,
|
emailTemplate: selectedTemplate,
|
||||||
recipients: defaultRecipients
|
...defaultRecipient
|
||||||
}}
|
}}
|
||||||
recipientCandidates={recipients}
|
recipientCandidates={recipients}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,242 +0,0 @@
|
|||||||
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
|
|
||||||
@ -251,8 +251,6 @@ const ObjectSelect = ({
|
|||||||
const masterFilterRef = useRef(masterFilter)
|
const masterFilterRef = useRef(masterFilter)
|
||||||
const clearedMissingValueRef = useRef(false)
|
const clearedMissingValueRef = useRef(false)
|
||||||
const loadGenerationRef = useRef(0)
|
const loadGenerationRef = useRef(0)
|
||||||
const loadedKeysRef = useRef(new Set())
|
|
||||||
const treeSelectValueRef = useRef(null)
|
|
||||||
const getSelectKey = useCallback(
|
const getSelectKey = useCallback(
|
||||||
(selectType, selectMasterFilter) =>
|
(selectType, selectMasterFilter) =>
|
||||||
`${selectType}::${JSON.stringify(selectMasterFilter ?? {})}`,
|
`${selectType}::${JSON.stringify(selectMasterFilter ?? {})}`,
|
||||||
@ -271,25 +269,31 @@ const ObjectSelect = ({
|
|||||||
|
|
||||||
// Normalize a value to an identity string so we can detect in-place _id updates
|
// Normalize a value to an identity string so we can detect in-place _id updates
|
||||||
const getValueIdentity = useCallback((val) => {
|
const getValueIdentity = useCallback((val) => {
|
||||||
if (val == null || val === '') return ''
|
if (val && typeof val === 'object') {
|
||||||
if (
|
// Handle arrays
|
||||||
typeof val === 'string' ||
|
if (Array.isArray(val)) {
|
||||||
typeof val === 'number' ||
|
const ids = val
|
||||||
typeof val === 'boolean'
|
.map((item) => {
|
||||||
) {
|
if (item && typeof item === 'object') {
|
||||||
return String(val).toLowerCase()
|
if (item._id) return String(item._id)
|
||||||
|
if (
|
||||||
|
item.value &&
|
||||||
|
typeof item.value === 'object' &&
|
||||||
|
item.value._id
|
||||||
|
)
|
||||||
|
return String(item.value._id)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
.sort()
|
||||||
|
return ids.length > 0 ? ids.join(',') : JSON.stringify(val)
|
||||||
|
}
|
||||||
|
// Handle single objects
|
||||||
|
if (val._id) return String(val._id)
|
||||||
|
if (val.value && typeof val.value === 'object' && val.value._id)
|
||||||
|
return String(val.value._id)
|
||||||
}
|
}
|
||||||
if (typeof val !== 'object') return String(val)
|
|
||||||
|
|
||||||
if (Array.isArray(val)) {
|
|
||||||
const ids = val
|
|
||||||
.map((item) => getValueIdentity(item))
|
|
||||||
.filter(Boolean)
|
|
||||||
.sort()
|
|
||||||
return ids.join(',')
|
|
||||||
}
|
|
||||||
if (val._id) return String(val._id).toLowerCase()
|
|
||||||
if (val.value != null) return getValueIdentity(val.value)
|
|
||||||
return JSON.stringify(val)
|
return JSON.stringify(val)
|
||||||
}, [])
|
}, [])
|
||||||
const prevValueIdentityRef = useRef(getValueIdentity(value))
|
const prevValueIdentityRef = useRef(getValueIdentity(value))
|
||||||
@ -422,7 +426,11 @@ const ObjectSelect = ({
|
|||||||
masterFilterRef.current
|
masterFilterRef.current
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
silentReloadRef.current?.()
|
if (itemExists) {
|
||||||
|
reloadRef.current?.()
|
||||||
|
} else {
|
||||||
|
silentReloadRef.current?.()
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -437,7 +445,7 @@ const ObjectSelect = ({
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const newEventHandler = useCallback(() => {
|
const newEventHandler = useCallback(() => {
|
||||||
silentReloadRef.current?.()
|
reloadRef.current?.()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const reloadRef = useRef(null)
|
const reloadRef = useRef(null)
|
||||||
@ -528,8 +536,8 @@ const ObjectSelect = ({
|
|||||||
newFilterPath,
|
newFilterPath,
|
||||||
objects
|
objects
|
||||||
)
|
)
|
||||||
const loaded =
|
const resolvedChildren =
|
||||||
nodeChildren.length > 0 || loadedKeysRef.current.has(nodeKey)
|
nodeChildren.length === 0 ? undefined : nodeChildren
|
||||||
const modelProperty = getModelProperty(type, property)
|
const modelProperty = getModelProperty(type, property)
|
||||||
return {
|
return {
|
||||||
title: <ObjectProperty {...modelProperty} value={value} />,
|
title: <ObjectProperty {...modelProperty} value={value} />,
|
||||||
@ -541,8 +549,7 @@ const ObjectSelect = ({
|
|||||||
filterPath: newFilterPath,
|
filterPath: newFilterPath,
|
||||||
selectable: false,
|
selectable: false,
|
||||||
isLeaf: false,
|
isLeaf: false,
|
||||||
loaded,
|
children: resolvedChildren
|
||||||
children: loaded ? nodeChildren : undefined
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
@ -601,7 +608,6 @@ const ObjectSelect = ({
|
|||||||
async (node) => {
|
async (node) => {
|
||||||
if (!node.property) return
|
if (!node.property) return
|
||||||
if (type == 'unknown') return
|
if (type == 'unknown') return
|
||||||
if (node.key) loadedKeysRef.current.add(node.key)
|
|
||||||
await handleFetchObjectsProperties(buildFilterFromNode(node))
|
await handleFetchObjectsProperties(buildFilterFromNode(node))
|
||||||
},
|
},
|
||||||
[buildFilterFromNode, handleFetchObjectsProperties, type]
|
[buildFilterFromNode, handleFetchObjectsProperties, type]
|
||||||
@ -690,13 +696,6 @@ const ObjectSelect = ({
|
|||||||
|
|
||||||
const onTreeSelectChange = useCallback(
|
const onTreeSelectChange = useCallback(
|
||||||
(nextValue) => {
|
(nextValue) => {
|
||||||
if (
|
|
||||||
getValueIdentity(nextValue) ===
|
|
||||||
getValueIdentity(treeSelectValueRef.current)
|
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const isEmptySelection = multiple
|
const isEmptySelection = multiple
|
||||||
? !Array.isArray(nextValue) || nextValue.length === 0
|
? !Array.isArray(nextValue) || nextValue.length === 0
|
||||||
: nextValue == null || nextValue === ''
|
: nextValue == null || nextValue === ''
|
||||||
@ -745,7 +744,7 @@ const ObjectSelect = ({
|
|||||||
}
|
}
|
||||||
onChange?.(null)
|
onChange?.(null)
|
||||||
},
|
},
|
||||||
[multiple, onChange, findObjectById, getValueIdentity]
|
[multiple, onChange, findObjectById]
|
||||||
)
|
)
|
||||||
|
|
||||||
const onSearch = useCallback(
|
const onSearch = useCallback(
|
||||||
@ -857,10 +856,6 @@ const ObjectSelect = ({
|
|||||||
treeDataRef.current = treeData
|
treeDataRef.current = treeData
|
||||||
}, [treeData])
|
}, [treeData])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
treeSelectValueRef.current = treeSelectValue
|
|
||||||
}, [treeSelectValue])
|
|
||||||
|
|
||||||
const prevValuesRef = useRef({ type, masterFilter })
|
const prevValuesRef = useRef({ type, masterFilter })
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -882,7 +877,6 @@ const ObjectSelect = ({
|
|||||||
treeDataRef.current = []
|
treeDataRef.current = []
|
||||||
setTreeVersion((v) => v + 1)
|
setTreeVersion((v) => v + 1)
|
||||||
setExpandedKeys([])
|
setExpandedKeys([])
|
||||||
loadedKeysRef.current = new Set()
|
|
||||||
setInitialized(false)
|
setInitialized(false)
|
||||||
valueRef.current = null
|
valueRef.current = null
|
||||||
setTreeSelectValue(null)
|
setTreeSelectValue(null)
|
||||||
@ -905,16 +899,13 @@ const ObjectSelect = ({
|
|||||||
const changeSource = isInternalChangeRef.current ? 'internal' : 'external'
|
const changeSource = isInternalChangeRef.current ? 'internal' : 'external'
|
||||||
|
|
||||||
if (changeSource == 'external') {
|
if (changeSource == 'external') {
|
||||||
const nextId = getValueId(value)
|
loadGenerationRef.current += 1
|
||||||
const alreadyInTree = isValueInTree(treeDataRef.current, nextId)
|
setObjectPropertiesTree({})
|
||||||
const alreadySelected =
|
setTreeData([])
|
||||||
treeSelectValueRef.current != null &&
|
treeDataRef.current = []
|
||||||
getValueIdentity(treeSelectValueRef.current) === currentValueIdentity
|
setInitialized(false)
|
||||||
|
setInitialLoading(true)
|
||||||
if (!alreadyInTree && !alreadySelected) {
|
valueRef.current = null
|
||||||
loadGenerationRef.current += 1
|
|
||||||
}
|
|
||||||
|
|
||||||
clearedMissingValueRef.current = false
|
clearedMissingValueRef.current = false
|
||||||
setValueNotFound(false)
|
setValueNotFound(false)
|
||||||
}
|
}
|
||||||
@ -933,40 +924,6 @@ const ObjectSelect = ({
|
|||||||
const generation = loadGenerationRef.current
|
const generation = loadGenerationRef.current
|
||||||
const handleValue = async () => {
|
const handleValue = async () => {
|
||||||
if (generation !== loadGenerationRef.current) return
|
if (generation !== loadGenerationRef.current) return
|
||||||
|
|
||||||
const valueIdentity = getValueIdentity(value)
|
|
||||||
const ids = multiple
|
|
||||||
? (Array.isArray(value) ? value.map(getValueId) : [])
|
|
||||||
: value == null
|
|
||||||
? []
|
|
||||||
: [getValueId(value)]
|
|
||||||
const allInTree =
|
|
||||||
ids.length > 0 &&
|
|
||||||
ids.every(
|
|
||||||
(id) =>
|
|
||||||
id == null ||
|
|
||||||
id === '' ||
|
|
||||||
isValueInTree(treeDataRef.current, id) ||
|
|
||||||
findObjectById(id) != null
|
|
||||||
)
|
|
||||||
|
|
||||||
if (
|
|
||||||
value != null &&
|
|
||||||
type != 'unknown' &&
|
|
||||||
allInTree &&
|
|
||||||
getValueIdentity(valueRef.current) !== valueIdentity
|
|
||||||
) {
|
|
||||||
valueRef.current = value
|
|
||||||
setTreeSelectValue(
|
|
||||||
multiple
|
|
||||||
? ids.map((id) => toSelectValue(id)).filter((id) => id != null)
|
|
||||||
: toSelectValue(ids[0])
|
|
||||||
)
|
|
||||||
setInitialized(true)
|
|
||||||
setInitialLoading(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
multiple &&
|
multiple &&
|
||||||
Array.isArray(value) &&
|
Array.isArray(value) &&
|
||||||
@ -1018,7 +975,6 @@ const ObjectSelect = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
setExpandedKeys([...new Set(pathKeys)])
|
setExpandedKeys([...new Set(pathKeys)])
|
||||||
pathKeys.forEach((key) => loadedKeysRef.current.add(key))
|
|
||||||
setTreeSelectValue(
|
setTreeSelectValue(
|
||||||
value
|
value
|
||||||
.map((item) => toSelectValue(getValueId(item)))
|
.map((item) => toSelectValue(getValueId(item)))
|
||||||
@ -1069,7 +1025,6 @@ const ObjectSelect = ({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
setExpandedKeys(pathKeys)
|
setExpandedKeys(pathKeys)
|
||||||
pathKeys.forEach((key) => loadedKeysRef.current.add(key))
|
|
||||||
const data = await handleFetchObjectsProperties(valueFilter)
|
const data = await handleFetchObjectsProperties(valueFilter)
|
||||||
if (generation !== loadGenerationRef.current) return
|
if (generation !== loadGenerationRef.current) return
|
||||||
if (Array.isArray(data)) applyTreeFromData(data)
|
if (Array.isArray(data)) applyTreeFromData(data)
|
||||||
@ -1114,8 +1069,7 @@ const ObjectSelect = ({
|
|||||||
connected,
|
connected,
|
||||||
getValueIdentity,
|
getValueIdentity,
|
||||||
multiple,
|
multiple,
|
||||||
applyTreeFromData,
|
applyTreeFromData
|
||||||
findObjectById
|
|
||||||
])
|
])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@ -5,7 +5,7 @@ const normalizeType = (type) =>
|
|||||||
? `${type.charAt(0).toLowerCase()}${type.slice(1)}`
|
? `${type.charAt(0).toLowerCase()}${type.slice(1)}`
|
||||||
: type
|
: type
|
||||||
|
|
||||||
export const getReferenceId = (value) => {
|
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,8 +40,7 @@ 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
|
||||||
}
|
}
|
||||||
@ -72,8 +71,7 @@ export const resolveEmailRecipients = async ({
|
|||||||
}`,
|
}`,
|
||||||
property: property.name,
|
property: property.name,
|
||||||
recipientType: resolvedType,
|
recipientType: resolvedType,
|
||||||
recipient,
|
recipient
|
||||||
object: objectData
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -81,15 +79,6 @@ 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,
|
||||||
@ -97,53 +86,40 @@ 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 item of list) {
|
for (const objectData 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
|
||||||
})
|
})
|
||||||
if (result.length) {
|
for (const candidate of result) {
|
||||||
for (const candidate of result) {
|
addCandidate(merged, candidate)
|
||||||
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 = []) => {
|
||||||
const recipients = candidatesToRecipients(candidates)
|
if (!Array.isArray(candidates) || !candidates.length) return null
|
||||||
if (!recipients.length) return null
|
|
||||||
|
|
||||||
const objectRecipient = recipients.find(
|
const objectCandidate = candidates.find(
|
||||||
(recipient) =>
|
(candidate) =>
|
||||||
(recipient.recipientType === 'client' ||
|
(candidate.recipientType === 'client' ||
|
||||||
recipient.recipientType === 'vendor') &&
|
candidate.recipientType === 'vendor') &&
|
||||||
recipient.recipient &&
|
candidate.recipient &&
|
||||||
recipient.recipientEmail
|
candidate.email
|
||||||
)
|
)
|
||||||
return objectRecipient || recipients[0]
|
if (objectCandidate) {
|
||||||
|
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,9 +11,11 @@ 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 {}
|
||||||
const first = list[0]
|
if (list.length === 1) {
|
||||||
if (first && typeof first === 'object' && !Array.isArray(first)) {
|
const first = list[0]
|
||||||
return { ...first, objects: list }
|
if (first && typeof first === 'object' && !Array.isArray(first)) {
|
||||||
|
return first
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return { objects: list }
|
return { objects: list }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,28 +15,7 @@ const NewEmailMessage = lazy(
|
|||||||
import('../../components/Dashboard/Management/EmailMessages/NewEmailMessage')
|
import('../../components/Dashboard/Management/EmailMessages/NewEmailMessage')
|
||||||
)
|
)
|
||||||
|
|
||||||
const readOnlyAfterCreate = (data, parentData) =>
|
const readOnlyAfterCreate = (data) => data?._id != null
|
||||||
(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',
|
||||||
@ -95,38 +74,49 @@ export const EmailMessage = {
|
|||||||
'_reference',
|
'_reference',
|
||||||
'name',
|
'name',
|
||||||
'state',
|
'state',
|
||||||
|
'recipientEmail',
|
||||||
'fromEmail',
|
'fromEmail',
|
||||||
|
'messageId',
|
||||||
'emailTemplate',
|
'emailTemplate',
|
||||||
'emailAccount',
|
'emailAccount',
|
||||||
'objectType',
|
'objectType',
|
||||||
'sentAt',
|
'sentAt',
|
||||||
'firstReadAt',
|
'readAt',
|
||||||
'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',
|
||||||
'firstReadAt',
|
'readAt',
|
||||||
'lastReadAt',
|
|
||||||
'createdAt',
|
'createdAt',
|
||||||
'updatedAt',
|
'updatedAt',
|
||||||
'_reference'
|
'_reference'
|
||||||
],
|
],
|
||||||
group: ['state', 'emailTemplate', 'emailAccount', 'objectType'],
|
group: [
|
||||||
|
'state',
|
||||||
|
'emailTemplate',
|
||||||
|
'emailAccount',
|
||||||
|
'objectType',
|
||||||
|
'recipientType'
|
||||||
|
],
|
||||||
sorters: [
|
sorters: [
|
||||||
'name',
|
'name',
|
||||||
'state',
|
'state',
|
||||||
|
'recipientEmail',
|
||||||
'fromEmail',
|
'fromEmail',
|
||||||
|
'messageId',
|
||||||
'sentAt',
|
'sentAt',
|
||||||
'firstReadAt',
|
'readAt',
|
||||||
'lastReadAt',
|
|
||||||
'createdAt',
|
'createdAt',
|
||||||
'updatedAt'
|
'updatedAt'
|
||||||
],
|
],
|
||||||
@ -181,18 +171,6 @@ 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
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -204,24 +182,19 @@ export const EmailMessage = {
|
|||||||
columnWidth: 240
|
columnWidth: 240
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'firstReadAt',
|
name: 'readAt',
|
||||||
label: 'First Read At',
|
label: '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',
|
||||||
@ -237,178 +210,7 @@ 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',
|
||||||
@ -426,6 +228,115 @@ 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