From 483682ce44243c96f36330a0f4a81824bf37a8a6 Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Sat, 12 Sep 2026 21:40:18 +0100 Subject: [PATCH] Add Email Account and Email Message Management Components - Introduced new EmailAccounts and EmailMessages components for managing email accounts and messages. - Implemented ObjectTable for displaying data with filtering and sorting capabilities. - Integrated InfoActionButtons for enhanced user interactions across various management functionalities. - Refactored existing components to replace DocumentPrintButton with InfoActionButtons for consistency in action handling. - Added EmailAccountInfo component for detailed view and management of individual email accounts. --- assets/icons/emailaccounticon.svg | 7 + assets/icons/mailtemplateicon.svg | 13 + .../Finance/Invoices/InvoiceInfo.jsx | 4 +- .../PaymentPolicies/PaymentPolicyInfo.jsx | 4 +- .../Finance/Payments/PaymentInfo.jsx | 4 +- .../Finance/TaxRecords/TaxRecordInfo.jsx | 4 +- .../FilamentStocks/FilamentStockInfo.jsx | 4 +- .../Inventory/OrderItems/OrderItemInfo.jsx | 6 + .../Inventory/PartStocks/PartStockInfo.jsx | 4 +- .../ProductStocks/ProductStockInfo.jsx | 4 +- .../PurchaseOrders/PurchaseOrderInfo.jsx | 4 +- .../Inventory/Shipments/PurchaseOrderInfo.jsx | 4 +- .../Inventory/Shipments/ShipmentInfo.jsx | 4 +- .../Inventory/StockAudits/StockAuditInfo.jsx | 4 +- .../StockLocations/StockLocationInfo.jsx | 4 +- .../StockTransfers/StockTransferInfo.jsx | 4 +- .../AppPasswords/AppPasswordInfo.jsx | 4 +- .../CourierServices/CourierServiceInfo.jsx | 4 +- .../Management/Couriers/CourierInfo.jsx | 4 +- .../DocumentJobs/DocumentJobInfo.jsx | 4 +- .../DocumentPrinters/DocumentPrinterInfo.jsx | 4 +- .../DocumentSizes/DocumentSizeInfo.jsx | 4 +- .../DocumentTemplateInfo.jsx | 4 +- .../Dashboard/Management/EmailAccounts.jsx | 83 +++++ .../EmailAccounts/EmailAccountInfo.jsx | 238 +++++++++++++ .../EmailAccounts/NewEmailAccount.jsx | 95 +++++ .../Dashboard/Management/EmailMessages.jsx | 83 +++++ .../EmailMessages/EmailMessageInfo.jsx | 227 ++++++++++++ .../EmailMessages/NewEmailMessage.jsx | 319 +++++++++++++++++ .../Dashboard/Management/EmailTemplates.jsx | 83 +++++ .../EmailTemplates/EmailTemplateDesign.jsx | 159 +++++++++ .../EmailTemplates/EmailTemplateInfo.jsx | 269 ++++++++++++++ .../EmailTemplates/NewEmailTemplate.jsx | 97 +++++ .../FilamentSkus/FilamentSkuInfo.jsx | 4 +- .../Management/Filaments/FilamentInfo.jsx | 4 +- .../Dashboard/Management/Files/FileInfo.jsx | 4 +- .../Dashboard/Management/Hosts/HostInfo.jsx | 4 +- .../Management/Materials/MaterialInfo.jsx | 4 +- .../Management/NoteTypes/NoteTypeInfo.jsx | 4 +- .../Dashboard/Management/Notes/NoteInfo.jsx | 4 +- .../Management/PartSkus/PartSkuInfo.jsx | 4 +- .../Dashboard/Management/Parts/PartInfo.jsx | 4 +- .../PermissionSettingInfo.jsx | 4 +- .../ProductCategories/ProductCategoryInfo.jsx | 4 +- .../Management/ProductSkus/ProductSkuInfo.jsx | 4 +- .../Management/Products/ProductInfo.jsx | 4 +- .../StockAuditLevels/StockAuditLevelInfo.jsx | 4 +- .../Management/TaxRates/TaxRateInfo.jsx | 4 +- .../Management/UserGroups/UserGroupInfo.jsx | 4 +- .../Dashboard/Management/Users/UserInfo.jsx | 4 +- .../Management/Vendors/VendorInfo.jsx | 4 +- .../FilamentProfiles/FilamentProfileInfo.jsx | 4 +- .../Production/GCodeFiles/GCodeFileInfo.jsx | 4 +- .../Dashboard/Production/Jobs/JobInfo.jsx | 4 +- .../PrinterProfiles/PrinterProfileInfo.jsx | 4 +- .../Production/Printers/PrinterInfo.jsx | 4 +- .../Production/SubJobs/SubJobInfo.jsx | 4 +- .../Dashboard/Sales/Clients/ClientInfo.jsx | 4 +- .../FulfillmentPolicyInfo.jsx | 4 +- .../ListingVarients/ListingVarientInfo.jsx | 4 +- .../Dashboard/Sales/Listings/ListingInfo.jsx | 4 +- .../Sales/Marketplaces/MarketplaceInfo.jsx | 4 +- .../Sales/ReturnPolicies/ReturnPolicyInfo.jsx | 4 +- .../Sales/SalesOrders/SalesOrderInfo.jsx | 4 +- .../Dashboard/common/EmailSendButton.jsx | 215 ++++++++++++ .../Dashboard/common/InfoActionButtons.jsx | 19 + .../Dashboard/common/ObjectProperty.jsx | 11 + .../Dashboard/common/TemplateEditor.jsx | 54 ++- .../Dashboard/common/TemplatePreview.jsx | 123 +++++-- .../Dashboard/context/ApiServerContext.jsx | 17 +- src/components/Dashboard/utils/Utils.js | 12 + .../Dashboard/utils/emailRecipients.js | 105 ++++++ src/components/Icons/EmailAccountIcon.jsx | 8 + src/components/Icons/EmailMessageIcon.jsx | 1 + src/components/Icons/EmailTemplateIcon.jsx | 8 + src/components/Icons/sidebarIconMap.jsx | 7 + src/database/ObjectModels.js | 9 + src/database/models/AuditLog.js | 12 +- src/database/models/EmailAccount.js | 233 ++++++++++++ src/database/models/EmailMessage.js | 331 ++++++++++++++++++ src/database/models/EmailTemplate.js | 313 +++++++++++++++++ src/database/sidebars/management.js | 25 ++ src/routes/ManagementRoutes.jsx | 33 ++ 83 files changed, 3266 insertions(+), 153 deletions(-) create mode 100644 assets/icons/emailaccounticon.svg create mode 100644 assets/icons/mailtemplateicon.svg create mode 100644 src/components/Dashboard/Management/EmailAccounts.jsx create mode 100644 src/components/Dashboard/Management/EmailAccounts/EmailAccountInfo.jsx create mode 100644 src/components/Dashboard/Management/EmailAccounts/NewEmailAccount.jsx create mode 100644 src/components/Dashboard/Management/EmailMessages.jsx create mode 100644 src/components/Dashboard/Management/EmailMessages/EmailMessageInfo.jsx create mode 100644 src/components/Dashboard/Management/EmailMessages/NewEmailMessage.jsx create mode 100644 src/components/Dashboard/Management/EmailTemplates.jsx create mode 100644 src/components/Dashboard/Management/EmailTemplates/EmailTemplateDesign.jsx create mode 100644 src/components/Dashboard/Management/EmailTemplates/EmailTemplateInfo.jsx create mode 100644 src/components/Dashboard/Management/EmailTemplates/NewEmailTemplate.jsx create mode 100644 src/components/Dashboard/common/EmailSendButton.jsx create mode 100644 src/components/Dashboard/common/InfoActionButtons.jsx create mode 100644 src/components/Dashboard/utils/emailRecipients.js create mode 100644 src/components/Icons/EmailAccountIcon.jsx create mode 100644 src/components/Icons/EmailMessageIcon.jsx create mode 100644 src/components/Icons/EmailTemplateIcon.jsx create mode 100644 src/database/models/EmailAccount.js create mode 100644 src/database/models/EmailMessage.js create mode 100644 src/database/models/EmailTemplate.js diff --git a/assets/icons/emailaccounticon.svg b/assets/icons/emailaccounticon.svg new file mode 100644 index 00000000..8019073d --- /dev/null +++ b/assets/icons/emailaccounticon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icons/mailtemplateicon.svg b/assets/icons/mailtemplateicon.svg new file mode 100644 index 00000000..41290340 --- /dev/null +++ b/assets/icons/mailtemplateicon.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/components/Dashboard/Finance/Invoices/InvoiceInfo.jsx b/src/components/Dashboard/Finance/Invoices/InvoiceInfo.jsx index 15adcb90..6b6023c8 100644 --- a/src/components/Dashboard/Finance/Invoices/InvoiceInfo.jsx +++ b/src/components/Dashboard/Finance/Invoices/InvoiceInfo.jsx @@ -24,7 +24,7 @@ import { useDashboardObjectTools } from '../../context/DashboardObjectToolsConte import ObjectActions from '../../common/ObjectActions.jsx' import ObjectTable from '../../common/ObjectTable.jsx' import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' -import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' +import InfoActionButtons from '../../common/InfoActionButtons.jsx' import UserNotifierToggle from '../../common/UserNotifierToggle.jsx' import ScrollBox from '../../common/ScrollBox.jsx' import { getModelProperty, getModelByName } from '../../../../database/ObjectModels.js' @@ -131,7 +131,7 @@ const InvoiceInfo = () => { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> + { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { visibleState={collapseState} updateVisibleState={updateCollapseState} /> - { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { + const tableRef = useRef() + + const [columnVisibility, setColumnVisibility] = + useColumnVisibility('emailAccount') + + const [showFilterSidebar, setShowFilterSidebar] = + useFilterSidebarVisibility('EmailAccounts') + + const [showSortSidebar, setShowSortSidebar] = + useSortSidebarVisibility('EmailAccounts') + + return ( + + + + + tableRef.current?.reload()} + /> + + + + + + setShowSortSidebar(!showSortSidebar)} + /> + setShowFilterSidebar(!showFilterSidebar)} + /> + + + + + + + + + ) +} + +export default EmailAccounts diff --git a/src/components/Dashboard/Management/EmailAccounts/EmailAccountInfo.jsx b/src/components/Dashboard/Management/EmailAccounts/EmailAccountInfo.jsx new file mode 100644 index 00000000..f885c91f --- /dev/null +++ b/src/components/Dashboard/Management/EmailAccounts/EmailAccountInfo.jsx @@ -0,0 +1,238 @@ +import { useRef, useState, useMemo } from 'react' +import { useLocation } from 'react-router-dom' +import { Card, Flex, Space, Spin } from 'antd' +import { LoadingOutlined } from '@ant-design/icons' +import loglevel from 'loglevel' +import config from '../../../../config.js' +import useCollapseState from '../../hooks/useCollapseState.jsx' +import NotesPanel from '../../common/NotesPanel.jsx' +import InfoCollapse from '../../common/InfoCollapse.jsx' +import ObjectInfo from '../../common/ObjectInfo.jsx' +import ViewButton from '../../common/ViewButton.jsx' +import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx' +import NoteIcon from '../../../Icons/NoteIcon.jsx' +import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx' +import ObjectForm from '../../common/ObjectForm.jsx' +import EditButtons from '../../common/EditButtons.jsx' +import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButtons.jsx' +import ActivityIndicator from '../../common/ActivityIndicator.jsx' +import ActionHandler from '../../common/ActionHandler.jsx' +import ObjectActions from '../../common/ObjectActions.jsx' +import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext' +import ObjectTable from '../../common/ObjectTable.jsx' +import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' +import InfoActionButtons from '../../common/InfoActionButtons.jsx' +import UserNotifierToggle from '../../common/UserNotifierToggle.jsx' +import { getModelByName } from '../../../../database/ObjectModels.js' +import ScrollBox from '../../common/ScrollBox.jsx' + +const log = loglevel.getLogger('EmailAccountInfo') +log.setLevel(config.logLevel) + +const EmailAccountInfo = () => { + const location = useLocation() + const objectFormRef = useRef(null) + const actionHandlerRef = useRef(null) + const emailAccountId = new URLSearchParams(location.search).get( + 'emailAccountId' + ) + const [collapseState, updateCollapseState] = useCollapseState( + 'EmailAccountInfo', + { + info: true, + notes: true, + auditLogs: false + } + ) + + const [objectFormState, setEditFormState] = useState({ + isEditing: false, + editLoading: false, + formValid: false, + activities: [], + loading: false, + editDisabled: false, + objectData: {} + }) + + const currentObjectTools = useMemo( + () => ( + + ), + [objectFormState.loading, objectFormState.isEditing, emailAccountId] + ) + useDashboardObjectTools(currentObjectTools) + + const actions = { + edit: () => { + objectFormRef?.current.startEditing() + return false + }, + cancelEdit: () => { + objectFormRef?.current.cancelEditing() + return true + }, + finishEdit: () => { + objectFormRef?.current.handleUpdate() + return true + } + } + + return ( + <> + + + + + + + + + + + + + { + actionHandlerRef.current.callAction('finishEdit') + }} + cancelEditing={() => { + actionHandlerRef.current.callAction('cancelEdit') + }} + startEditing={() => { + actionHandlerRef.current.callAction('edit') + }} + editLoading={objectFormState.editLoading} + formValid={objectFormState.formValid} + disabled={ + objectFormState.beingEditedByOther || + objectFormState.loading || + objectFormState.editDisabled + } + loading={objectFormState.editLoading} + /> + + + + + + + } + active={collapseState.info} + onToggle={(expanded) => updateCollapseState('info', expanded)} + collapseKey='info' + > + { + setEditFormState((prev) => ({ ...prev, ...state })) + }} + > + {({ loading, isEditing, objectData }) => ( + + )} + + + + + } + active={collapseState.notes} + onToggle={(expanded) => updateCollapseState('notes', expanded)} + collapseKey='notes' + > + } + > + + + + + + + } + active={collapseState.auditLogs} + onToggle={(expanded) => + updateCollapseState('auditLogs', expanded) + } + collapseKey='auditLogs' + > + {objectFormState.loading ? ( + + ) : ( + + )} + + + + + + ) +} + +export default EmailAccountInfo diff --git a/src/components/Dashboard/Management/EmailAccounts/NewEmailAccount.jsx b/src/components/Dashboard/Management/EmailAccounts/NewEmailAccount.jsx new file mode 100644 index 00000000..83bd42a9 --- /dev/null +++ b/src/components/Dashboard/Management/EmailAccounts/NewEmailAccount.jsx @@ -0,0 +1,95 @@ +import PropTypes from 'prop-types' +import ObjectInfo from '../../common/ObjectInfo' +import NewObjectForm from '../../common/NewObjectForm' +import WizardView from '../../common/WizardView' + +const NewEmailAccount = ({ onOk, defaultValues }) => { + return ( + + {({ handleSubmit, submitLoading, objectData, formValid }) => { + const steps = [ + { + title: 'Required', + key: 'required', + content: ( + + ) + }, + { + title: 'Optional', + key: 'optional', + content: ( + + ) + }, + { + title: 'Summary', + key: 'summary', + content: ( + + ) + } + ] + + return ( + { + const result = await handleSubmit() + if (result) { + onOk() + } + }} + /> + ) + }} + + ) +} + +NewEmailAccount.propTypes = { + onOk: PropTypes.func.isRequired, + reset: PropTypes.bool, + defaultValues: PropTypes.object +} + +export default NewEmailAccount diff --git a/src/components/Dashboard/Management/EmailMessages.jsx b/src/components/Dashboard/Management/EmailMessages.jsx new file mode 100644 index 00000000..a08d90fd --- /dev/null +++ b/src/components/Dashboard/Management/EmailMessages.jsx @@ -0,0 +1,83 @@ +import { useRef } from 'react' +import { Flex, Space } from 'antd' +import ObjectTable from '../common/ObjectTable' +import ObjectActions from '../common/ObjectActions' +import useColumnVisibility from '../hooks/useColumnVisibility' +import ObjectTableViewButton from '../common/ObjectTableViewButton' +import FilterSidebarButton from '../common/FilterSidebarButton' +import SortSidebarButton from '../common/SortSidebarButton' +import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility' +import useSortSidebarVisibility from '../hooks/useSortSidebarVisibility' +import ColumnViewButton from '../common/ColumnViewButton' +import ExportListButton from '../common/ExportListButton' +import ListViewTabs from '../common/ListViewTabs' +import ListViewEditButtons from '../common/ListViewEditButtons' +import { ObjectListViewProvider } from '../context/ObjectListViewContext' + +const EmailMessages = () => { + const tableRef = useRef() + + const [columnVisibility, setColumnVisibility] = + useColumnVisibility('emailMessage') + + const [showFilterSidebar, setShowFilterSidebar] = + useFilterSidebarVisibility('EmailMessages') + + const [showSortSidebar, setShowSortSidebar] = + useSortSidebarVisibility('EmailMessages') + + return ( + + + + + tableRef.current?.reload()} + /> + + + + + + setShowSortSidebar(!showSortSidebar)} + /> + setShowFilterSidebar(!showFilterSidebar)} + /> + + + + + + + + + ) +} + +export default EmailMessages diff --git a/src/components/Dashboard/Management/EmailMessages/EmailMessageInfo.jsx b/src/components/Dashboard/Management/EmailMessages/EmailMessageInfo.jsx new file mode 100644 index 00000000..3f89ecfc --- /dev/null +++ b/src/components/Dashboard/Management/EmailMessages/EmailMessageInfo.jsx @@ -0,0 +1,227 @@ +import { useRef, useState, useMemo } from 'react' +import { useLocation } from 'react-router-dom' +import { Card, Flex, Space, Spin } from 'antd' +import { LoadingOutlined } from '@ant-design/icons' +import loglevel from 'loglevel' +import config from '../../../../config.js' +import useCollapseState from '../../hooks/useCollapseState.jsx' +import NotesPanel from '../../common/NotesPanel.jsx' +import InfoCollapse from '../../common/InfoCollapse.jsx' +import ObjectInfo from '../../common/ObjectInfo.jsx' +import ViewButton from '../../common/ViewButton.jsx' +import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx' +import NoteIcon from '../../../Icons/NoteIcon.jsx' +import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx' +import ObjectForm from '../../common/ObjectForm.jsx' +import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButtons.jsx' +import ActivityIndicator from '../../common/ActivityIndicator.jsx' +import ObjectActions from '../../common/ObjectActions.jsx' +import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext' +import ObjectTable from '../../common/ObjectTable.jsx' +import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' +import InfoActionButtons from '../../common/InfoActionButtons.jsx' +import UserNotifierToggle from '../../common/UserNotifierToggle.jsx' +import { getModelByName } from '../../../../database/ObjectModels.js' +import ScrollBox from '../../common/ScrollBox.jsx' +import TemplatePreview from '../../common/TemplatePreview.jsx' + +const log = loglevel.getLogger('EmailMessageInfo') +log.setLevel(config.logLevel) + +const EmailMessageInfo = () => { + const location = useLocation() + const objectFormRef = useRef(null) + const emailMessageId = new URLSearchParams(location.search).get( + 'emailMessageId' + ) + const [collapseState, updateCollapseState] = useCollapseState( + 'EmailMessageInfo', + { + info: true, + notes: true, + auditLogs: false + } + ) + + const [objectFormState, setEditFormState] = useState({ + isEditing: false, + editLoading: false, + formValid: false, + activities: [], + loading: false, + editDisabled: false, + objectData: {} + }) + + const currentObjectTools = useMemo( + () => ( + + ), + [objectFormState.loading, emailMessageId] + ) + useDashboardObjectTools(currentObjectTools) + + return ( + <> + + + + + + + + + + + + + + + + { + setEditFormState((prev) => ({ ...prev, ...state })) + }} + > + {({ loading, isEditing, objectData }) => ( + + } + active={collapseState.info} + onToggle={(expanded) => + updateCollapseState('info', expanded) + } + collapseKey='info' + > + + + } + active={collapseState.content} + onToggle={(expanded) => + updateCollapseState('content', expanded) + } + collapseKey='content' + > + } + > + + + + + + + )} + + + } + active={collapseState.notes} + onToggle={(expanded) => updateCollapseState('notes', expanded)} + collapseKey='notes' + > + } + > + + + + + + + } + active={collapseState.auditLogs} + onToggle={(expanded) => + updateCollapseState('auditLogs', expanded) + } + collapseKey='auditLogs' + > + {objectFormState.loading ? ( + + ) : ( + + )} + + + + + + ) +} + +export default EmailMessageInfo diff --git a/src/components/Dashboard/Management/EmailMessages/NewEmailMessage.jsx b/src/components/Dashboard/Management/EmailMessages/NewEmailMessage.jsx new file mode 100644 index 00000000..2c86d6d2 --- /dev/null +++ b/src/components/Dashboard/Management/EmailMessages/NewEmailMessage.jsx @@ -0,0 +1,319 @@ +import PropTypes from 'prop-types' +import { + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState +} from 'react' +import { Modal } from 'antd' +import ObjectInfo from '../../common/ObjectInfo' +import NewObjectForm from '../../common/NewObjectForm' +import WizardView from '../../common/WizardView' +import TemplatePreview from '../../common/TemplatePreview' +import ProgressDisplay from '../../common/ProgressDisplay' +import { ApiServerContext } from '../../context/ApiServerContext' +import { resolveEmailRecipients } from '../../utils/emailRecipients' + +const progressByState = { + queued: 10, + rendering: 35, + sending: 70, + sent: 100, + error: 100 +} +const EMPTY_RECIPIENT_CANDIDATES = [] +const EMPTY_DEFAULT_VALUES = {} + +const EmailWizardContent = ({ + accounts, + defaultCandidates, + handleSubmit, + submitLoading, + objectData, + setObjectData, + form, + formValid, + onCreated +}) => { + const { fetchObject } = useContext(ApiServerContext) + const [candidates, setCandidates] = useState(defaultCandidates) + + useEffect(() => { + if (defaultCandidates.length) { + setCandidates(defaultCandidates) + return + } + let cancelled = false + resolveEmailRecipients({ + type: objectData?.objectType, + objectData: objectData?.object, + fetchObject + }).then((result) => { + if (!cancelled) setCandidates(result) + }) + return () => { + cancelled = true + } + }, [ + defaultCandidates, + fetchObject, + objectData?.object, + objectData?.objectType + ]) + + useEffect(() => { + setObjectData((previous) => ({ + ...previous, + _recipientCandidates: candidates + })) + }, [candidates, setObjectData]) + + useEffect(() => { + const currentAccount = objectData?.emailAccount + const account = + accounts.find( + (entry) => entry._id === (currentAccount?._id || currentAccount) + ) || accounts[0] + if (!account || objectData?.emailAccount) return + form.setFieldsValue({ emailAccount: account, fromEmail: account.fromEmail }) + setObjectData((previous) => ({ + ...previous, + emailAccount: account, + fromEmail: account.fromEmail + })) + }, [accounts, form, objectData?.emailAccount, setObjectData]) + + useEffect(() => { + if (!candidates.length || objectData?.recipientEmail) return + const candidate = candidates[0] + const values = { + recipientEmail: candidate.email, + recipientType: candidate.recipientType, + recipient: candidate.recipient + } + form.setFieldsValue(values) + setObjectData((previous) => ({ ...previous, ...values })) + }, [candidates, form, objectData?.recipientEmail, setObjectData]) + + return ( + + ) + } + ]} + submitText='Send' + title='Send Email' + formValid={ + formValid && Boolean(objectData?.recipientEmail) && accounts.length > 0 + } + loading={submitLoading} + sideBarGrow + sideBar={ +
+ +
+ } + onSubmit={async () => { + const result = await handleSubmit() + if (result?._id) onCreated(result) + }} + /> + ) +} + +EmailWizardContent.propTypes = { + accounts: PropTypes.array.isRequired, + defaultCandidates: PropTypes.array.isRequired, + handleSubmit: PropTypes.func.isRequired, + submitLoading: PropTypes.bool, + objectData: PropTypes.object, + setObjectData: PropTypes.func.isRequired, + form: PropTypes.object.isRequired, + formValid: PropTypes.bool, + onCreated: PropTypes.func.isRequired +} + +const NewEmailMessage = ({ + onOk, + defaultValues = EMPTY_DEFAULT_VALUES, + recipientCandidates = EMPTY_RECIPIENT_CANDIDATES +}) => { + const { + fetchObject, + fetchObjects, + connected, + subscribeToObjectUpdates, + subscribeToObjectTypeUpdates + } = useContext(ApiServerContext) + const [accounts, setAccounts] = useState([]) + const [sendingMessage, setSendingMessage] = useState(null) + const fetchObjectRef = useRef(fetchObject) + const fetchObjectsRef = useRef(fetchObjects) + const fetchedMessageIdRef = useRef(null) + fetchObjectRef.current = fetchObject + fetchObjectsRef.current = fetchObjects + const initialValues = useMemo( + () => ({ read: false, ...defaultValues }), + [defaultValues] + ) + + const loadAccounts = useCallback(async () => { + const result = await fetchObjectsRef.current('emailAccount', { + filter: { active: true }, + limit: 100 + }) + setAccounts(Array.isArray(result?.data) ? result.data : []) + }, []) + + useEffect(() => { + loadAccounts() + }, [loadAccounts]) + + useEffect(() => { + if (!connected) return undefined + return subscribeToObjectTypeUpdates( + 'emailAccount', + { active: true }, + loadAccounts + ) + }, [connected, loadAccounts, subscribeToObjectTypeUpdates]) + + useEffect(() => { + if (!connected) return undefined + const unsubscribes = accounts + .filter((account) => account?._id) + .map((account) => + subscribeToObjectUpdates( + account._id.toLowerCase(), + 'emailAccount', + (update) => { + const merged = { ...account, ...update } + setAccounts((previous) => + merged.active === true + ? previous.map((entry) => + entry._id === account._id ? merged : entry + ) + : previous.filter((entry) => entry._id !== account._id) + ) + } + ) + ) + return () => unsubscribes.forEach((unsubscribe) => unsubscribe?.()) + }, [accounts, connected, subscribeToObjectUpdates]) + + useEffect(() => { + const id = sendingMessage?._id + if (!id) return undefined + + let cancelled = false + if (fetchedMessageIdRef.current !== id) { + fetchedMessageIdRef.current = id + fetchObjectRef.current(id, 'emailMessage').then((current) => { + if (!cancelled && current?._id) { + setSendingMessage((previous) => ({ ...previous, ...current })) + } + }) + } + + const unsubscribe = + connected === true + ? subscribeToObjectUpdates(id.toLowerCase(), 'emailMessage', (update) => + setSendingMessage((previous) => ({ ...previous, ...update })) + ) + : undefined + + return () => { + cancelled = true + unsubscribe?.() + } + }, [connected, sendingMessage?._id, subscribeToObjectUpdates]) + + useEffect(() => { + if (sendingMessage?.state?.type === 'sent') onOk?.(sendingMessage) + }, [onOk, sendingMessage]) + + return ( + <> + + {(formProps) => ( + + )} + + setSendingMessage(null)} + maskClosable={false} + centered + destroyOnHidden + zIndex={2000} + width={360} + getContainer={() => document.body} + > + + {sendingMessage?.state?.message || + `Email ${sendingMessage?.state?.type || 'queued'}...`} + + + + ) +} + +NewEmailMessage.propTypes = { + onOk: PropTypes.func, + defaultValues: PropTypes.object, + recipientCandidates: PropTypes.array +} + +export default NewEmailMessage diff --git a/src/components/Dashboard/Management/EmailTemplates.jsx b/src/components/Dashboard/Management/EmailTemplates.jsx new file mode 100644 index 00000000..3caf16f8 --- /dev/null +++ b/src/components/Dashboard/Management/EmailTemplates.jsx @@ -0,0 +1,83 @@ +import { useRef } from 'react' +import { Flex, Space } from 'antd' +import ObjectTable from '../common/ObjectTable' +import ObjectActions from '../common/ObjectActions' +import useColumnVisibility from '../hooks/useColumnVisibility' +import ObjectTableViewButton from '../common/ObjectTableViewButton' +import FilterSidebarButton from '../common/FilterSidebarButton' +import SortSidebarButton from '../common/SortSidebarButton' +import useFilterSidebarVisibility from '../hooks/useFilterSidebarVisibility' +import useSortSidebarVisibility from '../hooks/useSortSidebarVisibility' +import ColumnViewButton from '../common/ColumnViewButton' +import ExportListButton from '../common/ExportListButton' +import ListViewTabs from '../common/ListViewTabs' +import ListViewEditButtons from '../common/ListViewEditButtons' +import { ObjectListViewProvider } from '../context/ObjectListViewContext' + +const EmailTemplates = () => { + const tableRef = useRef() + + const [columnVisibility, setColumnVisibility] = + useColumnVisibility('emailTemplate') + + const [showFilterSidebar, setShowFilterSidebar] = + useFilterSidebarVisibility('EmailTemplates') + + const [showSortSidebar, setShowSortSidebar] = + useSortSidebarVisibility('EmailTemplates') + + return ( + + + + + tableRef.current?.reload()} + /> + + + + + + setShowSortSidebar(!showSortSidebar)} + /> + setShowFilterSidebar(!showFilterSidebar)} + /> + + + + + + + + + ) +} + +export default EmailTemplates diff --git a/src/components/Dashboard/Management/EmailTemplates/EmailTemplateDesign.jsx b/src/components/Dashboard/Management/EmailTemplates/EmailTemplateDesign.jsx new file mode 100644 index 00000000..c8f314ae --- /dev/null +++ b/src/components/Dashboard/Management/EmailTemplates/EmailTemplateDesign.jsx @@ -0,0 +1,159 @@ +import { useMemo, useRef, useState } from 'react' +import { useLocation } from 'react-router-dom' +import { Card, Flex, Space, Spin } from 'antd' +import { LoadingOutlined } from '@ant-design/icons' +import useCollapseState from '../../hooks/useCollapseState' +import NotesPanel from '../../common/NotesPanel' +import InfoCollapse from '../../common/InfoCollapse' +import ViewButton from '../../common/ViewButton' +import NoteIcon from '../../../Icons/NoteIcon' +import ObjectForm from '../../common/ObjectForm' +import EditButtons from '../../common/EditButtons' +import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButtons' +import ActivityIndicator from '../../common/ActivityIndicator' +import ActionHandler from '../../common/ActionHandler' +import ObjectActions from '../../common/ObjectActions' +import TemplateEditor from '../../common/TemplateEditor' +import ScrollBox from '../../common/ScrollBox' +import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext' + +const EmailTemplateDesign = () => { + const location = useLocation() + const objectFormRef = useRef(null) + const actionHandlerRef = useRef(null) + const emailTemplateId = new URLSearchParams(location.search).get( + 'emailTemplateId' + ) + const [collapseState, updateCollapseState] = useCollapseState( + 'EmailTemplateDesign', + { preview: true, editor: true, notes: true } + ) + const [formState, setFormState] = useState({ + isEditing: false, + editLoading: false, + formValid: false, + activities: [], + loading: false, + objectData: {} + }) + + useDashboardObjectTools( + useMemo( + () => ( + + ), + [emailTemplateId, formState.isEditing, formState.loading] + ) + ) + + return ( + + + + + + + + + + actionHandlerRef.current.callAction('finishEdit')} + cancelEditing={() => + actionHandlerRef.current.callAction('cancelEdit') + } + startEditing={() => actionHandlerRef.current.callAction('edit')} + editLoading={formState.editLoading} + formValid={formState.formValid} + disabled={formState.beingEditedByOther || formState.loading} + loading={formState.editLoading} + /> + + + + { + objectFormRef.current?.startEditing() + return false + }, + cancelEdit: () => { + objectFormRef.current?.cancelEditing() + return true + }, + finishEdit: () => { + objectFormRef.current?.handleUpdate() + return true + } + }} + loading={formState.loading} + ref={actionHandlerRef} + > + + setFormState((previous) => ({ ...previous, ...state })) + } + > + {({ loading, isEditing, objectData, form, setObjectData }) => ( + + )} + + + } + active={collapseState.notes} + onToggle={(expanded) => updateCollapseState('notes', expanded)} + collapseKey='notes' + > + }> + + + + + + + + + ) +} + +export default EmailTemplateDesign diff --git a/src/components/Dashboard/Management/EmailTemplates/EmailTemplateInfo.jsx b/src/components/Dashboard/Management/EmailTemplates/EmailTemplateInfo.jsx new file mode 100644 index 00000000..c215e14d --- /dev/null +++ b/src/components/Dashboard/Management/EmailTemplates/EmailTemplateInfo.jsx @@ -0,0 +1,269 @@ +import { useRef, useState, useMemo } from 'react' +import { useLocation } from 'react-router-dom' +import { Card, Flex, Space, Spin } from 'antd' +import { LoadingOutlined } from '@ant-design/icons' +import loglevel from 'loglevel' +import config from '../../../../config.js' +import useCollapseState from '../../hooks/useCollapseState.jsx' +import NotesPanel from '../../common/NotesPanel.jsx' +import InfoCollapse from '../../common/InfoCollapse.jsx' +import ObjectInfo from '../../common/ObjectInfo.jsx' +import ObjectProperty from '../../common/ObjectProperty.jsx' +import ViewButton from '../../common/ViewButton.jsx' +import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx' +import NoteIcon from '../../../Icons/NoteIcon.jsx' +import AuditLogIcon from '../../../Icons/AuditLogIcon.jsx' +import FileIcon from '../../../Icons/FileIcon.jsx' +import ObjectForm from '../../common/ObjectForm.jsx' +import EditButtons from '../../common/EditButtons.jsx' +import ObjectTableNavigationButtons from '../../common/ObjectTableNavigationButtons.jsx' +import ActivityIndicator from '../../common/ActivityIndicator.jsx' +import ActionHandler from '../../common/ActionHandler.jsx' +import ObjectActions from '../../common/ObjectActions.jsx' +import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext' +import ObjectTable from '../../common/ObjectTable.jsx' +import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' +import InfoActionButtons from '../../common/InfoActionButtons.jsx' +import UserNotifierToggle from '../../common/UserNotifierToggle.jsx' +import { getModelByName, getModelProperty } from '../../../../database/ObjectModels.js' +import ScrollBox from '../../common/ScrollBox.jsx' + +const log = loglevel.getLogger('EmailTemplateInfo') +log.setLevel(config.logLevel) + +const EmailTemplateInfo = () => { + const location = useLocation() + const objectFormRef = useRef(null) + const actionHandlerRef = useRef(null) + const emailTemplateId = new URLSearchParams(location.search).get( + 'emailTemplateId' + ) + const [collapseState, updateCollapseState] = useCollapseState( + 'EmailTemplateInfo', + { + info: true, + attachments: true, + notes: true, + auditLogs: false + } + ) + + const [objectFormState, setEditFormState] = useState({ + isEditing: false, + editLoading: false, + formValid: false, + activities: [], + loading: false, + editDisabled: false, + objectData: {} + }) + + const currentObjectTools = useMemo( + () => ( + + ), + [objectFormState.loading, objectFormState.isEditing, emailTemplateId] + ) + useDashboardObjectTools(currentObjectTools) + + const actions = { + edit: () => { + objectFormRef?.current.startEditing() + return false + }, + cancelEdit: () => { + objectFormRef?.current.cancelEditing() + return true + }, + finishEdit: () => { + objectFormRef?.current.handleUpdate() + return true + } + } + + return ( + <> + + + + + + + + + + + + + { + actionHandlerRef.current.callAction('finishEdit') + }} + cancelEditing={() => { + actionHandlerRef.current.callAction('cancelEdit') + }} + startEditing={() => { + actionHandlerRef.current.callAction('edit') + }} + editLoading={objectFormState.editLoading} + formValid={objectFormState.formValid} + disabled={ + objectFormState.beingEditedByOther || + objectFormState.loading || + objectFormState.editDisabled + } + loading={objectFormState.editLoading} + /> + + + + + + + { + setEditFormState((prev) => ({ ...prev, ...state })) + }} + > + {({ loading, isEditing, objectData }) => ( + + } + active={collapseState.info} + onToggle={(expanded) => + updateCollapseState('info', expanded) + } + collapseKey='info' + > + } + isEditing={isEditing} + type='emailTemplate' + objectData={objectData} + visibleProperties={{ + content: false, + testObject: false, + attachments: false + }} + labelWidth='210px' + /> + + } + active={collapseState.attachments} + onToggle={(expanded) => + updateCollapseState('attachments', expanded) + } + collapseKey='attachments' + > + + + + )} + + + + } + active={collapseState.notes} + onToggle={(expanded) => updateCollapseState('notes', expanded)} + collapseKey='notes' + > + } + > + + + + + + + } + active={collapseState.auditLogs} + onToggle={(expanded) => + updateCollapseState('auditLogs', expanded) + } + collapseKey='auditLogs' + > + {objectFormState.loading ? ( + + ) : ( + + )} + + + + + + ) +} + +export default EmailTemplateInfo diff --git a/src/components/Dashboard/Management/EmailTemplates/NewEmailTemplate.jsx b/src/components/Dashboard/Management/EmailTemplates/NewEmailTemplate.jsx new file mode 100644 index 00000000..d5bca084 --- /dev/null +++ b/src/components/Dashboard/Management/EmailTemplates/NewEmailTemplate.jsx @@ -0,0 +1,97 @@ +import PropTypes from 'prop-types' +import ObjectInfo from '../../common/ObjectInfo' +import NewObjectForm from '../../common/NewObjectForm' +import WizardView from '../../common/WizardView' + +const NewEmailTemplate = ({ onOk, defaultValues }) => { + return ( + + {({ handleSubmit, submitLoading, objectData, formValid }) => { + const steps = [ + { + title: 'Required', + key: 'required', + content: ( + + ) + }, + { + title: 'Optional', + key: 'optional', + content: ( + + ) + }, + { + title: 'Summary', + key: 'summary', + content: ( + + ) + } + ] + + return ( + { + const result = await handleSubmit() + if (result) { + onOk() + } + }} + /> + ) + }} + + ) +} + +NewEmailTemplate.propTypes = { + onOk: PropTypes.func.isRequired, + reset: PropTypes.bool, + defaultValues: PropTypes.object +} + +export default NewEmailTemplate diff --git a/src/components/Dashboard/Management/FilamentSkus/FilamentSkuInfo.jsx b/src/components/Dashboard/Management/FilamentSkus/FilamentSkuInfo.jsx index 301a04bf..f0eaee4d 100644 --- a/src/components/Dashboard/Management/FilamentSkus/FilamentSkuInfo.jsx +++ b/src/components/Dashboard/Management/FilamentSkus/FilamentSkuInfo.jsx @@ -19,7 +19,7 @@ import ObjectActions from '../../common/ObjectActions.jsx' import { useDashboardObjectTools } from '../../context/DashboardObjectToolsContext' import ObjectTable from '../../common/ObjectTable.jsx' import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx' -import DocumentPrintButton from '../../common/DocumentPrintButton.jsx' +import InfoActionButtons from '../../common/InfoActionButtons.jsx' import UserNotifierToggle from '../../common/UserNotifierToggle.jsx' import { getModelByName } from '../../../../database/ObjectModels.js' import ScrollBox from '../../common/ScrollBox.jsx' @@ -109,7 +109,7 @@ const FilamentSkuInfo = () => { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { objectData={objectFormState.objectData} disabled={objectFormState.loading} /> - { + const { + fetchObject, + fetchObjects, + connected, + subscribeToObjectUpdates, + subscribeToObjectTypeUpdates + } = useContext(ApiServerContext) + const { token } = useContext(AuthContext) + const [templates, setTemplates] = useState([]) + const [recipients, setRecipients] = useState([]) + const [selectedTemplate, setSelectedTemplate] = useState(null) + const [open, setOpen] = useState(false) + const [loading, setLoading] = useState(false) + const fetchObjectsRef = useRef(fetchObjects) + const fetchObjectRef = useRef(fetchObject) + fetchObjectsRef.current = fetchObjects + fetchObjectRef.current = fetchObject + + const filter = useMemo( + () => ({ objectType: type, global: false, active: true }), + [type] + ) + const matches = useCallback( + (template) => + template?.objectType === type && + template?.global === false && + template?.active === true, + [type] + ) + + const loadTemplates = useCallback( + async (silent = false) => { + if (!type || token == null) return + if (!silent) setLoading(true) + try { + const result = await fetchObjectsRef.current('emailTemplate', { + filter, + limit: 100 + }) + setTemplates(Array.isArray(result?.data) ? result.data : []) + } finally { + if (!silent) setLoading(false) + } + }, + [filter, token, type] + ) + + useEffect(() => { + loadTemplates() + }, [loadTemplates]) + + useEffect(() => { + let cancelled = false + setRecipients([]) + resolveEmailRecipients({ + type, + objectData, + fetchObject: (...args) => fetchObjectRef.current(...args) + }).then((result) => { + if (!cancelled) setRecipients(result) + }) + return () => { + cancelled = true + } + }, [objectData, type]) + + useEffect(() => { + if (!connected) return undefined + return subscribeToObjectTypeUpdates('emailTemplate', filter, () => + loadTemplates(true) + ) + }, [ + connected, + filter, + loadTemplates, + subscribeToObjectTypeUpdates + ]) + + useEffect(() => { + if (!connected) return undefined + const unsubscribes = templates + .filter((template) => template?._id) + .map((template) => + subscribeToObjectUpdates( + template._id.toLowerCase(), + 'emailTemplate', + (update) => { + setTemplates((previous) => { + const merged = { ...template, ...update } + if (!matches(merged)) { + return previous.filter((entry) => entry._id !== template._id) + } + return previous.map((entry) => + entry._id === template._id ? merged : entry + ) + }) + } + ) + ) + return () => unsubscribes.forEach((unsubscribe) => unsubscribe?.()) + }, [connected, matches, subscribeToObjectUpdates, templates]) + + const menuItems = useMemo(() => { + const byTag = new Map() + const untagged = [] + const makeItem = (template) => ({ + key: template._id, + label: template.name, + icon: , + onClick: () => { + setSelectedTemplate(template) + setOpen(true) + } + }) + + templates.forEach((template) => { + const tags = Array.isArray(template.tags) ? template.tags.filter(Boolean) : [] + if (!tags.length) { + untagged.push(template) + } else { + tags.forEach((tag) => byTag.set(tag, [...(byTag.get(tag) || []), template])) + } + }) + + return [ + ...untagged + .slice() + .sort((a, b) => a.name.localeCompare(b.name)) + .map(makeItem), + ...[...byTag.entries()] + .sort(([a], [b]) => a.localeCompare(b)) + .map(([tag, taggedTemplates]) => ({ + key: tag, + label: tag, + children: taggedTemplates + .slice() + .sort((a, b) => a.name.localeCompare(b.name)) + .map(makeItem) + })) + ] + }, [templates]) + + const defaultRecipient = useMemo( + () => getDefaultEmailRecipient(recipients), + [recipients] + ) + + const unavailable = disabled || loading || !templates.length || !recipients.length + const title = !recipients.length + ? 'No email recipients available for this object' + : !templates.length + ? 'No email templates available for this object type' + : 'Send email' + + return ( + <> + +