@antelopejs/dms-mailing 0.3.4 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +232 -22
- package/dist/automation/email-event-trigger.js +2 -2
- package/dist/automation/email-event-trigger.js.map +1 -1
- package/dist/automation/send-template-action.js +2 -2
- package/dist/automation/send-template-action.js.map +1 -1
- package/dist/config.js +3 -1
- package/dist/config.js.map +1 -1
- package/dist/constants.d.ts +10 -1
- package/dist/constants.js +15 -3
- package/dist/constants.js.map +1 -1
- package/dist/crons/attention.d.ts +3 -0
- package/dist/crons/attention.js +19 -0
- package/dist/crons/attention.js.map +1 -0
- package/dist/crons/index.d.ts +2 -0
- package/dist/crons/index.js +2 -0
- package/dist/crons/index.js.map +1 -1
- package/dist/crons/jobs.d.ts +12 -0
- package/dist/crons/jobs.js +27 -0
- package/dist/crons/jobs.js.map +1 -0
- package/dist/crons/retention.d.ts +2 -4
- package/dist/crons/retention.js +7 -22
- package/dist/crons/retention.js.map +1 -1
- package/dist/data/displays.d.ts +5 -0
- package/dist/data/displays.js +23 -0
- package/dist/data/displays.js.map +1 -0
- package/dist/data/index.d.ts +1 -0
- package/dist/data/index.js +1 -0
- package/dist/data/index.js.map +1 -1
- package/dist/data/locale-fallback.d.ts +3 -0
- package/dist/data/locale-fallback.js +20 -0
- package/dist/data/locale-fallback.js.map +1 -0
- package/dist/data/read-only-routes.d.ts +7 -0
- package/dist/data/read-only-routes.js +13 -0
- package/dist/data/read-only-routes.js.map +1 -0
- package/dist/data/sends-table.d.ts +19 -5
- package/dist/data/sends-table.js +185 -17
- package/dist/data/sends-table.js.map +1 -1
- package/dist/data/suppressions-table.d.ts +24 -0
- package/dist/data/suppressions-table.js +120 -0
- package/dist/data/suppressions-table.js.map +1 -0
- package/dist/data/templates-table.d.ts +20 -2
- package/dist/data/templates-table.js +188 -31
- package/dist/data/templates-table.js.map +1 -1
- package/dist/data-types/categories-type.d.ts +11 -13
- package/dist/data-types/categories-type.js +3 -3
- package/dist/data-types/categories-type.js.map +1 -1
- package/dist/data-types/category-type.js +1 -1
- package/dist/data-types/category-type.js.map +1 -1
- package/dist/db/models/index.d.ts +3 -0
- package/dist/db/models/index.js +3 -0
- package/dist/db/models/index.js.map +1 -1
- package/dist/db/models/sends.model.d.ts +12 -1
- package/dist/db/models/sends.model.js +64 -0
- package/dist/db/models/sends.model.js.map +1 -1
- package/dist/db/models/settings.model.d.ts +0 -1
- package/dist/db/models/settings.model.js +0 -7
- package/dist/db/models/settings.model.js.map +1 -1
- package/dist/db/models/suppressions.model.d.ts +28 -0
- package/dist/db/models/suppressions.model.js +46 -0
- package/dist/db/models/suppressions.model.js.map +1 -0
- package/dist/db/models/template-versions.model.d.ts +24 -0
- package/dist/db/models/template-versions.model.js +17 -0
- package/dist/db/models/template-versions.model.js.map +1 -0
- package/dist/db/models/templates.model.d.ts +2 -0
- package/dist/db/models/templates.model.js +3 -0
- package/dist/db/models/templates.model.js.map +1 -1
- package/dist/db/models/webhook-status.model.d.ts +24 -0
- package/dist/db/models/webhook-status.model.js +21 -0
- package/dist/db/models/webhook-status.model.js.map +1 -0
- package/dist/db/tables/index.d.ts +3 -0
- package/dist/db/tables/index.js +3 -0
- package/dist/db/tables/index.js.map +1 -1
- package/dist/db/tables/send-events.table.d.ts +2 -2
- package/dist/db/tables/send-events.table.js.map +1 -1
- package/dist/db/tables/sends.table.d.ts +10 -2
- package/dist/db/tables/sends.table.js +34 -1
- package/dist/db/tables/sends.table.js.map +1 -1
- package/dist/db/tables/suppressions.table.d.ts +12 -0
- package/dist/db/tables/suppressions.table.js +53 -0
- package/dist/db/tables/suppressions.table.js.map +1 -0
- package/dist/db/tables/template-versions.table.d.ts +13 -0
- package/dist/db/tables/template-versions.table.js +59 -0
- package/dist/db/tables/template-versions.table.js.map +1 -0
- package/dist/db/tables/templates.table.d.ts +11 -0
- package/dist/db/tables/templates.table.js +45 -0
- package/dist/db/tables/templates.table.js.map +1 -1
- package/dist/db/tables/webhook-status.table.d.ts +14 -0
- package/dist/db/tables/webhook-status.table.js +58 -0
- package/dist/db/tables/webhook-status.table.js.map +1 -0
- package/dist/engine/changes.d.ts +12 -0
- package/dist/engine/changes.js +107 -0
- package/dist/engine/changes.js.map +1 -0
- package/dist/engine/conditions.js +0 -2
- package/dist/engine/conditions.js.map +1 -1
- package/dist/engine/coverage.d.ts +1 -0
- package/dist/engine/coverage.js +6 -2
- package/dist/engine/coverage.js.map +1 -1
- package/dist/engine/fallback-html.d.ts +1 -1
- package/dist/engine/fallback-html.js +51 -34
- package/dist/engine/fallback-html.js.map +1 -1
- package/dist/engine/index.d.ts +3 -0
- package/dist/engine/index.js +3 -0
- package/dist/engine/index.js.map +1 -1
- package/dist/engine/plain-text.d.ts +3 -0
- package/dist/engine/plain-text.js +60 -0
- package/dist/engine/plain-text.js.map +1 -0
- package/dist/engine/resolve.d.ts +2 -1
- package/dist/engine/resolve.js +17 -9
- package/dist/engine/resolve.js.map +1 -1
- package/dist/engine/system-variables.d.ts +10 -0
- package/dist/engine/system-variables.js +70 -0
- package/dist/engine/system-variables.js.map +1 -0
- package/dist/engine/tokens.d.ts +2 -1
- package/dist/engine/tokens.js +6 -2
- package/dist/engine/tokens.js.map +1 -1
- package/dist/hooks/tenant-export.js +13 -1
- package/dist/hooks/tenant-export.js.map +1 -1
- package/dist/implementations/dms-mailing/index.d.ts +1 -1
- package/dist/implementations/dms-mailing/index.js +2 -2
- package/dist/implementations/dms-mailing/index.js.map +1 -1
- package/dist/index.js +13 -11
- package/dist/index.js.map +1 -1
- package/dist/pages/editor.js +9 -2
- package/dist/pages/editor.js.map +1 -1
- package/dist/pages/index.d.ts +1 -0
- package/dist/pages/index.js +1 -0
- package/dist/pages/index.js.map +1 -1
- package/dist/pages/module.d.ts +2 -0
- package/dist/pages/module.js +26 -2
- package/dist/pages/module.js.map +1 -1
- package/dist/pages/overview.d.ts +2 -6
- package/dist/pages/overview.js +109 -52
- package/dist/pages/overview.js.map +1 -1
- package/dist/pages/sends.d.ts +3 -4
- package/dist/pages/sends.js +197 -60
- package/dist/pages/sends.js.map +1 -1
- package/dist/pages/settings/form.js +114 -53
- package/dist/pages/settings/form.js.map +1 -1
- package/dist/pages/settings/page.d.ts +0 -1
- package/dist/pages/settings/page.js +8 -11
- package/dist/pages/settings/page.js.map +1 -1
- package/dist/pages/suppressions.d.ts +9 -0
- package/dist/pages/suppressions.js +151 -0
- package/dist/pages/suppressions.js.map +1 -0
- package/dist/pages/templates.d.ts +0 -1
- package/dist/pages/templates.js +114 -40
- package/dist/pages/templates.js.map +1 -1
- package/dist/permissions.js +2 -2
- package/dist/permissions.js.map +1 -1
- package/dist/quick-actions.js +7 -4
- package/dist/quick-actions.js.map +1 -1
- package/dist/routes/events.d.ts +8 -5
- package/dist/routes/events.js +55 -24
- package/dist/routes/events.js.map +1 -1
- package/dist/routes/index.d.ts +2 -0
- package/dist/routes/index.js +2 -0
- package/dist/routes/index.js.map +1 -1
- package/dist/routes/metrics.d.ts +60 -23
- package/dist/routes/metrics.js +231 -83
- package/dist/routes/metrics.js.map +1 -1
- package/dist/routes/period.d.ts +2 -1
- package/dist/routes/period.js +9 -3
- package/dist/routes/period.js.map +1 -1
- package/dist/routes/sends.d.ts +49 -8
- package/dist/routes/sends.js +239 -24
- package/dist/routes/sends.js.map +1 -1
- package/dist/routes/settings.d.ts +44 -5
- package/dist/routes/settings.js +190 -15
- package/dist/routes/settings.js.map +1 -1
- package/dist/routes/suppressions.d.ts +16 -0
- package/dist/routes/suppressions.js +92 -0
- package/dist/routes/suppressions.js.map +1 -0
- package/dist/routes/template-versions.d.ts +28 -0
- package/dist/routes/template-versions.js +87 -0
- package/dist/routes/template-versions.js.map +1 -0
- package/dist/routes/templates.d.ts +60 -6
- package/dist/routes/templates.js +204 -35
- package/dist/routes/templates.js.map +1 -1
- package/dist/services/attention.d.ts +19 -6
- package/dist/services/attention.js +177 -12
- package/dist/services/attention.js.map +1 -1
- package/dist/services/catalog.d.ts +3 -0
- package/dist/services/catalog.js +25 -0
- package/dist/services/catalog.js.map +1 -0
- package/dist/services/categories.d.ts +8 -32
- package/dist/services/categories.js +14 -25
- package/dist/services/categories.js.map +1 -1
- package/dist/services/checked-send.d.ts +9 -0
- package/dist/services/checked-send.js +54 -0
- package/dist/services/checked-send.js.map +1 -0
- package/dist/services/events.d.ts +5 -0
- package/dist/services/events.js +102 -9
- package/dist/services/events.js.map +1 -1
- package/dist/services/metrics.d.ts +17 -6
- package/dist/services/metrics.js +57 -18
- package/dist/services/metrics.js.map +1 -1
- package/dist/services/notify.d.ts +18 -10
- package/dist/services/notify.js +82 -37
- package/dist/services/notify.js.map +1 -1
- package/dist/services/provider-banner.d.ts +14 -0
- package/dist/services/provider-banner.js +115 -0
- package/dist/services/provider-banner.js.map +1 -0
- package/dist/services/render.js.map +1 -1
- package/dist/services/send-stat-items.d.ts +7 -0
- package/dist/services/send-stat-items.js +127 -0
- package/dist/services/send-stat-items.js.map +1 -0
- package/dist/services/send-stats.d.ts +23 -0
- package/dist/services/send-stats.js +63 -0
- package/dist/services/send-stats.js.map +1 -0
- package/dist/services/send.d.ts +18 -3
- package/dist/services/send.js +81 -17
- package/dist/services/send.js.map +1 -1
- package/dist/services/settings.d.ts +5 -0
- package/dist/services/settings.js +28 -7
- package/dist/services/settings.js.map +1 -1
- package/dist/services/starters.d.ts +15 -0
- package/dist/services/starters.js +264 -0
- package/dist/services/starters.js.map +1 -0
- package/dist/services/suppressions.d.ts +20 -0
- package/dist/services/suppressions.js +53 -0
- package/dist/services/suppressions.js.map +1 -0
- package/dist/services/template-attention.d.ts +4 -0
- package/dist/services/template-attention.js +43 -0
- package/dist/services/template-attention.js.map +1 -0
- package/dist/services/template-stats.d.ts +27 -0
- package/dist/services/template-stats.js +59 -0
- package/dist/services/template-stats.js.map +1 -0
- package/dist/services/templates.d.ts +31 -4
- package/dist/services/templates.js +240 -21
- package/dist/services/templates.js.map +1 -1
- package/dist/services/webhook-status.d.ts +27 -0
- package/dist/services/webhook-status.js +134 -0
- package/dist/services/webhook-status.js.map +1 -0
- package/dist/types/blocks.d.ts +1 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -1
- package/dist/types/sends.d.ts +10 -1
- package/dist/types/sends.js +35 -2
- package/dist/types/sends.js.map +1 -1
- package/dist/types/suppressions.d.ts +9 -0
- package/dist/types/suppressions.js +10 -0
- package/dist/types/suppressions.js.map +1 -0
- package/dist/types/webhook.d.ts +17 -0
- package/dist/types/webhook.js +3 -0
- package/dist/types/webhook.js.map +1 -0
- package/dist/validation/settings.schema.d.ts +144 -9
- package/dist/validation/settings.schema.js +113 -16
- package/dist/validation/settings.schema.js.map +1 -1
- package/dist/validation/templates.schema.d.ts +137 -10
- package/dist/validation/templates.schema.js +28 -2
- package/dist/validation/templates.schema.js.map +1 -1
- package/frontend-vue/app/build/components/FormFieldRow.vue +90 -0
- package/frontend-vue/app/build/components/FormRows.vue +26 -0
- package/frontend-vue/app/build/components/RequiredLegend.vue +11 -0
- package/frontend-vue/app/components/CategoriesInput.vue +323 -77
- package/frontend-vue/app/components/CategoryInput.vue +13 -14
- package/frontend-vue/app/components/CategoryLabel.vue +34 -0
- package/frontend-vue/app/components/DuplicateModal.vue +201 -0
- package/frontend-vue/app/components/Editor.vue +55 -20
- package/frontend-vue/app/components/FixAddressModal.vue +133 -0
- package/frontend-vue/app/components/FunnelCard.vue +146 -43
- package/frontend-vue/app/components/LocaleSelect.vue +98 -0
- package/frontend-vue/app/components/NewTemplateModal.vue +388 -102
- package/frontend-vue/app/components/NewTemplateSourceTile.vue +70 -0
- package/frontend-vue/app/components/OverviewGate.vue +220 -0
- package/frontend-vue/app/components/PageHeader.vue +98 -0
- package/frontend-vue/app/components/RealSendModal.vue +555 -0
- package/frontend-vue/app/components/RecipientsField.vue +41 -0
- package/frontend-vue/app/components/RenderingModal.vue +189 -0
- package/frontend-vue/app/components/RetentionInput.vue +212 -0
- package/frontend-vue/app/components/SendDrawer.vue +542 -209
- package/frontend-vue/app/components/StarterGallery.vue +153 -0
- package/frontend-vue/app/components/SystemVariables.vue +63 -0
- package/frontend-vue/app/components/TemplateCard.vue +218 -57
- package/frontend-vue/app/components/TemplateDrawer.vue +785 -274
- package/frontend-vue/app/components/TemplateGallery.vue +312 -56
- package/frontend-vue/app/components/TemplatePreviewFrame.vue +112 -22
- package/frontend-vue/app/components/TestSendModal.vue +266 -95
- package/frontend-vue/app/components/TokenText.vue +61 -10
- package/frontend-vue/app/components/VariablePath.vue +20 -0
- package/frontend-vue/app/components/VersionHistory.vue +196 -0
- package/frontend-vue/app/components/VersionPreview.vue +164 -0
- package/frontend-vue/app/components/WebhookStatus.vue +130 -0
- package/frontend-vue/app/components/blocks/Button.vue +1 -1
- package/frontend-vue/app/components/blocks/Code.vue +1 -1
- package/frontend-vue/app/components/blocks/Footer.vue +56 -7
- package/frontend-vue/app/components/blocks/Heading.vue +1 -1
- package/frontend-vue/app/components/blocks/Hero.vue +13 -7
- package/frontend-vue/app/components/blocks/List.vue +50 -6
- package/frontend-vue/app/components/blocks/Paragraph.vue +1 -1
- package/frontend-vue/app/components/blocks/Total.vue +3 -3
- package/frontend-vue/app/components/editor/EditorAutocompleteMenu.vue +50 -0
- package/frontend-vue/app/components/editor/EditorBlock.vue +37 -90
- package/frontend-vue/app/components/editor/EditorBlockFields.vue +140 -0
- package/frontend-vue/app/components/editor/EditorBlockInspector.vue +92 -0
- package/frontend-vue/app/components/editor/EditorBlockList.vue +32 -25
- package/frontend-vue/app/components/editor/EditorBlockPicker.vue +58 -0
- package/frontend-vue/app/components/editor/EditorBlockTools.vue +71 -0
- package/frontend-vue/app/components/editor/EditorCanvas.vue +105 -70
- package/frontend-vue/app/components/editor/EditorCompare.vue +211 -0
- package/frontend-vue/app/components/editor/EditorConditionFields.vue +56 -46
- package/frontend-vue/app/components/editor/EditorConditionInspector.vue +54 -0
- package/frontend-vue/app/components/editor/EditorEnvelope.vue +77 -0
- package/frontend-vue/app/components/editor/EditorIfBlock.vue +36 -86
- package/frontend-vue/app/components/editor/EditorInsertPoint.vue +64 -0
- package/frontend-vue/app/components/editor/EditorInspector.vue +80 -0
- package/frontend-vue/app/components/editor/EditorLeftPane.vue +103 -0
- package/frontend-vue/app/components/editor/EditorLifecycle.vue +67 -0
- package/frontend-vue/app/components/editor/EditorLoading.vue +23 -0
- package/frontend-vue/app/components/editor/EditorLocaleSwitch.vue +61 -0
- package/frontend-vue/app/components/editor/EditorMissingLocale.vue +72 -48
- package/frontend-vue/app/components/editor/EditorMissingPolicy.vue +42 -0
- package/frontend-vue/app/components/editor/EditorNotice.vue +127 -0
- package/frontend-vue/app/components/editor/EditorOutline.vue +65 -0
- package/frontend-vue/app/components/editor/EditorPalette.vue +95 -42
- package/frontend-vue/app/components/editor/EditorPaletteGroup.vue +66 -0
- package/frontend-vue/app/components/editor/EditorPublishReview.vue +102 -0
- package/frontend-vue/app/components/editor/EditorSaveState.vue +88 -0
- package/frontend-vue/app/components/editor/EditorShortcutHint.vue +17 -0
- package/frontend-vue/app/components/editor/EditorSimChip.vue +51 -0
- package/frontend-vue/app/components/editor/EditorTemplatePane.vue +131 -0
- package/frontend-vue/app/components/editor/EditorTestDataPane.vue +98 -0
- package/frontend-vue/app/components/editor/EditorTitle.vue +82 -0
- package/frontend-vue/app/components/editor/EditorTokenField.vue +224 -0
- package/frontend-vue/app/components/editor/EditorToolbar.vue +34 -134
- package/frontend-vue/app/components/editor/EditorVariableForm.vue +100 -0
- package/frontend-vue/app/components/editor/EditorVariableRow.vue +87 -0
- package/frontend-vue/app/components/editor/EditorVariablesPane.vue +161 -0
- package/frontend-vue/app/components/editor/EditorViewSwitches.vue +76 -0
- package/frontend-vue/app/components/editor/EditorVisibility.vue +101 -0
- package/frontend-vue/app/components/editor/EditorWorkspace.vue +62 -149
- package/frontend-vue/app/composables/useBlockContext.ts +16 -0
- package/frontend-vue/app/composables/useEditorActions.ts +381 -0
- package/frontend-vue/app/composables/useEditorAnnouncements.ts +51 -0
- package/frontend-vue/app/composables/useEditorContext.ts +30 -23
- package/frontend-vue/app/composables/useEditorSession.ts +299 -0
- package/frontend-vue/app/composables/useEditorShortcuts.ts +81 -0
- package/frontend-vue/app/composables/useLocalizedDefaults.ts +17 -0
- package/frontend-vue/app/composables/useMailingApi.ts +142 -27
- package/frontend-vue/app/composables/useOverviewSetup.ts +13 -0
- package/frontend-vue/app/composables/useReplayToast.ts +72 -0
- package/frontend-vue/app/composables/useTemplateCategories.ts +35 -0
- package/frontend-vue/app/composables/useTemplateDrawer.ts +196 -62
- package/frontend-vue/app/composables/useTemplateEditor.ts +318 -86
- package/frontend-vue/app/emails/EmailMailingTemplate.vue +16 -4
- package/frontend-vue/app/emails/components/MailingBlocks.vue +96 -38
- package/frontend-vue/app/emails/components/MailingTextLines.vue +20 -0
- package/frontend-vue/app/plugins/column-displays.ts +65 -0
- package/frontend-vue/app/plugins/{template-gallery-display.client.ts → template-gallery-display.ts} +1 -1
- package/frontend-vue/app/types/mailing.ts +193 -18
- package/frontend-vue/app/utils/api-error.ts +33 -0
- package/frontend-vue/app/utils/autocomplete.ts +120 -0
- package/frontend-vue/app/utils/blocks/components.ts +10 -10
- package/frontend-vue/app/utils/blocks/dnd.ts +7 -3
- package/frontend-vue/app/utils/blocks/factory.ts +199 -38
- package/frontend-vue/app/utils/blocks/fields.ts +39 -19
- package/frontend-vue/app/utils/blocks/index.ts +1 -0
- package/frontend-vue/app/utils/blocks/outline.ts +91 -0
- package/frontend-vue/app/utils/blocks/tree.ts +114 -2
- package/frontend-vue/app/utils/categories.ts +201 -15
- package/frontend-vue/app/utils/changes.ts +152 -0
- package/frontend-vue/app/utils/conditions.ts +25 -19
- package/frontend-vue/app/utils/editor-checks.ts +266 -0
- package/frontend-vue/app/utils/editor-route.ts +43 -4
- package/frontend-vue/app/utils/funnel.ts +74 -0
- package/frontend-vue/app/utils/gallery.ts +179 -1
- package/frontend-vue/app/utils/history.ts +66 -0
- package/frontend-vue/app/utils/lifecycle.ts +185 -0
- package/frontend-vue/app/utils/locales.ts +107 -2
- package/frontend-vue/app/utils/overview-setup.ts +83 -0
- package/frontend-vue/app/utils/pages.ts +5 -0
- package/frontend-vue/app/utils/paths.ts +51 -15
- package/frontend-vue/app/utils/period.ts +62 -22
- package/frontend-vue/app/utils/problems.ts +181 -0
- package/frontend-vue/app/utils/recipients.ts +49 -0
- package/frontend-vue/app/utils/rendering-title.ts +23 -0
- package/frontend-vue/app/utils/send-actions.ts +207 -0
- package/frontend-vue/app/utils/send-link.ts +28 -0
- package/frontend-vue/app/utils/send-snippet.ts +97 -0
- package/frontend-vue/app/utils/send-stats.ts +24 -0
- package/frontend-vue/app/utils/send-values.ts +156 -0
- package/frontend-vue/app/utils/shortcuts.ts +113 -0
- package/frontend-vue/app/utils/slug.ts +82 -0
- package/frontend-vue/app/utils/starters.ts +23 -0
- package/frontend-vue/app/utils/suppression.ts +112 -0
- package/frontend-vue/app/utils/system-variables.ts +165 -0
- package/frontend-vue/app/utils/template-attention.ts +104 -0
- package/frontend-vue/app/utils/timeline.ts +221 -19
- package/frontend-vue/app/utils/tokens.ts +115 -13
- package/frontend-vue/app/utils/variable-types.ts +23 -0
- package/frontend-vue/app/utils/variables.ts +208 -1
- package/frontend-vue/app/utils/version-history.ts +53 -0
- package/frontend-vue/app/utils/webhook-status.ts +170 -0
- package/frontend-vue/dms.frontend.ts +19 -11
- package/frontend-vue/i18n/locales/mailing-en-GB.json +1173 -161
- package/frontend-vue/i18n/locales/mailing-fr-FR.json +1220 -208
- package/frontend-vue/package.json +2 -2
- package/frontend-vue/pnpm-lock.yaml +46 -161
- package/frontend-vue/tests/api-error.test.ts +27 -0
- package/frontend-vue/tests/autocomplete.test.ts +84 -0
- package/frontend-vue/tests/block-components.test.ts +1 -1
- package/frontend-vue/tests/block-fields.test.ts +12 -2
- package/frontend-vue/tests/blocks-factory.test.ts +46 -0
- package/frontend-vue/tests/blocks-outline.test.ts +102 -0
- package/frontend-vue/tests/blocks-tree.test.ts +28 -1
- package/frontend-vue/tests/categories-input.test.ts +139 -0
- package/frontend-vue/tests/categories.test.ts +148 -15
- package/frontend-vue/tests/changes.test.ts +136 -0
- package/frontend-vue/tests/column-displays.test.ts +73 -0
- package/frontend-vue/tests/conditions.test.ts +49 -19
- package/frontend-vue/tests/dnd.test.ts +10 -0
- package/frontend-vue/tests/editor-block-list.test.ts +24 -2
- package/frontend-vue/tests/editor-checks.test.ts +178 -0
- package/frontend-vue/tests/editor-route.test.ts +37 -9
- package/frontend-vue/tests/editor-session.test.ts +206 -0
- package/frontend-vue/tests/footer-block.test.ts +88 -0
- package/frontend-vue/tests/funnel-card.test.ts +116 -0
- package/frontend-vue/tests/funnel.test.ts +81 -0
- package/frontend-vue/tests/gallery.test.ts +124 -2
- package/frontend-vue/tests/helpers/nuxt-globals.ts +232 -0
- package/frontend-vue/tests/history.test.ts +48 -0
- package/frontend-vue/tests/i18n-editor.test.ts +38 -0
- package/frontend-vue/tests/i18n.test.ts +55 -0
- package/frontend-vue/tests/lifecycle.test.ts +145 -0
- package/frontend-vue/tests/locales.test.ts +60 -0
- package/frontend-vue/tests/localized-defaults.test.ts +54 -0
- package/frontend-vue/tests/new-template-modal.test.ts +210 -0
- package/frontend-vue/tests/overview-gate.test.ts +146 -0
- package/frontend-vue/tests/overview-setup.test.ts +78 -0
- package/frontend-vue/tests/paths.test.ts +23 -0
- package/frontend-vue/tests/period.test.ts +59 -10
- package/frontend-vue/tests/problems.test.ts +153 -0
- package/frontend-vue/tests/recipients.test.ts +47 -0
- package/frontend-vue/tests/rendering-title.test.ts +24 -0
- package/frontend-vue/tests/replay-toast.test.ts +67 -0
- package/frontend-vue/tests/retention-input.test.ts +129 -0
- package/frontend-vue/tests/send-actions.test.ts +128 -0
- package/frontend-vue/tests/send-modals.test.ts +415 -0
- package/frontend-vue/tests/send-snippet.test.ts +51 -0
- package/frontend-vue/tests/send-stats.test.ts +12 -0
- package/frontend-vue/tests/send-values.test.ts +119 -0
- package/frontend-vue/tests/shortcuts.test.ts +75 -0
- package/frontend-vue/tests/slug.test.ts +88 -0
- package/frontend-vue/tests/suppression.test.ts +159 -0
- package/frontend-vue/tests/system-variables.test.ts +133 -0
- package/frontend-vue/tests/template-attention.test.ts +123 -0
- package/frontend-vue/tests/template-drawer.test.ts +449 -0
- package/frontend-vue/tests/template-editor.test.ts +218 -9
- package/frontend-vue/tests/template-gallery-display.test.ts +32 -0
- package/frontend-vue/tests/template-gallery.test.ts +370 -0
- package/frontend-vue/tests/template-overlays.test.ts +125 -0
- package/frontend-vue/tests/timeline.test.ts +175 -20
- package/frontend-vue/tests/tokens.test.ts +79 -1
- package/frontend-vue/tests/variable-path.test.ts +22 -0
- package/frontend-vue/tests/variable-types.test.ts +20 -0
- package/frontend-vue/tests/variables.test.ts +90 -2
- package/frontend-vue/tests/version-history.test.ts +41 -0
- package/frontend-vue/tests/webhook-status.test.ts +108 -0
- package/package.json +11 -9
- package/src/automation/email-event-trigger.ts +3 -3
- package/src/automation/send-template-action.ts +7 -5
- package/src/config.ts +3 -1
- package/src/constants.ts +18 -2
- package/src/crons/attention.ts +21 -0
- package/src/crons/index.ts +2 -0
- package/src/crons/jobs.ts +35 -0
- package/src/crons/retention.ts +7 -22
- package/src/data/displays.ts +18 -0
- package/src/data/index.ts +1 -0
- package/src/data/locale-fallback.ts +26 -0
- package/src/data/read-only-routes.ts +14 -0
- package/src/data/sends-table.ts +204 -19
- package/src/data/suppressions-table.ts +127 -0
- package/src/data/templates-table.ts +295 -63
- package/src/data-types/categories-type.ts +4 -5
- package/src/data-types/category-type.ts +1 -1
- package/src/db/models/index.ts +3 -0
- package/src/db/models/sends.model.ts +102 -1
- package/src/db/models/settings.model.ts +2 -12
- package/src/db/models/suppressions.model.ts +53 -0
- package/src/db/models/template-versions.model.ts +23 -0
- package/src/db/models/templates.model.ts +17 -0
- package/src/db/models/webhook-status.model.ts +25 -0
- package/src/db/tables/index.ts +3 -0
- package/src/db/tables/send-events.table.ts +6 -2
- package/src/db/tables/sends.table.ts +37 -2
- package/src/db/tables/suppressions.table.ts +46 -0
- package/src/db/tables/template-versions.table.ts +44 -0
- package/src/db/tables/templates.table.ts +67 -5
- package/src/db/tables/webhook-status.table.ts +44 -0
- package/src/engine/changes.ts +198 -0
- package/src/engine/conditions.ts +1 -7
- package/src/engine/coverage.ts +7 -2
- package/src/engine/fallback-html.ts +80 -42
- package/src/engine/index.ts +3 -0
- package/src/engine/plain-text.ts +86 -0
- package/src/engine/resolve.ts +49 -15
- package/src/engine/system-variables.ts +124 -0
- package/src/engine/tokens.ts +13 -1
- package/src/hooks/tenant-export.ts +21 -2
- package/src/implementations/dms-mailing/index.ts +1 -1
- package/src/index.ts +29 -13
- package/src/pages/editor.ts +13 -2
- package/src/pages/index.ts +6 -3
- package/src/pages/module.ts +31 -2
- package/src/pages/overview.ts +187 -128
- package/src/pages/sends.ts +271 -112
- package/src/pages/settings/form.ts +144 -54
- package/src/pages/settings/page.ts +9 -17
- package/src/pages/suppressions.ts +157 -0
- package/src/pages/templates.ts +139 -42
- package/src/permissions.ts +6 -4
- package/src/quick-actions.ts +12 -5
- package/src/routes/events.ts +110 -33
- package/src/routes/index.ts +2 -0
- package/src/routes/metrics.ts +348 -98
- package/src/routes/period.ts +23 -6
- package/src/routes/sends.ts +413 -41
- package/src/routes/settings.ts +291 -26
- package/src/routes/suppressions.ts +87 -0
- package/src/routes/template-versions.ts +116 -0
- package/src/routes/templates.ts +334 -48
- package/src/services/attention.ts +283 -30
- package/src/services/catalog.ts +28 -0
- package/src/services/categories.ts +28 -27
- package/src/services/checked-send.ts +123 -0
- package/src/services/events.ts +174 -20
- package/src/services/metrics.ts +137 -28
- package/src/services/notify.ts +126 -44
- package/src/services/provider-banner.ts +165 -0
- package/src/services/render.ts +1 -0
- package/src/services/send-stat-items.ts +160 -0
- package/src/services/send-stats.ts +122 -0
- package/src/services/send.ts +193 -31
- package/src/services/settings.ts +52 -8
- package/src/services/starters.ts +383 -0
- package/src/services/suppressions.ts +116 -0
- package/src/services/template-attention.ts +82 -0
- package/src/services/template-stats.ts +97 -0
- package/src/services/templates.ts +400 -37
- package/src/services/webhook-status.ts +202 -0
- package/src/types/blocks.ts +6 -0
- package/src/types/index.ts +2 -0
- package/src/types/sends.ts +64 -2
- package/src/types/suppressions.ts +27 -0
- package/src/types/webhook.ts +29 -0
- package/src/validation/settings.schema.ts +173 -17
- package/src/validation/templates.schema.ts +53 -1
- package/dist/antelope-modules.d.ts +0 -32
- package/dist/antelope-modules.js +0 -36
- package/dist/antelope-modules.js.map +0 -1
- package/dist/test/antelope.test.d.ts +0 -2
- package/dist/test/antelope.test.js +0 -180
- package/dist/test/antelope.test.js.map +0 -1
- package/dist/test/helpers/http.d.ts +0 -4
- package/dist/test/helpers/http.js +0 -28
- package/dist/test/helpers/http.js.map +0 -1
- package/dist/test/helpers/owner.d.ts +0 -5
- package/dist/test/helpers/owner.js +0 -30
- package/dist/test/helpers/owner.js.map +0 -1
- package/dist/test/integration/events.test.d.ts +0 -1
- package/dist/test/integration/events.test.js +0 -143
- package/dist/test/integration/events.test.js.map +0 -1
- package/dist/test/integration/retention.test.d.ts +0 -1
- package/dist/test/integration/retention.test.js +0 -291
- package/dist/test/integration/retention.test.js.map +0 -1
- package/dist/test/integration/sends.test.d.ts +0 -1
- package/dist/test/integration/sends.test.js +0 -210
- package/dist/test/integration/sends.test.js.map +0 -1
- package/dist/test/integration/settings.test.d.ts +0 -1
- package/dist/test/integration/settings.test.js +0 -137
- package/dist/test/integration/settings.test.js.map +0 -1
- package/dist/test/integration/setup.test.d.ts +0 -1
- package/dist/test/integration/setup.test.js +0 -32
- package/dist/test/integration/setup.test.js.map +0 -1
- package/dist/test/integration/templates.test.d.ts +0 -1
- package/dist/test/integration/templates.test.js +0 -233
- package/dist/test/integration/templates.test.js.map +0 -1
- package/dist/test/integration/tenant-export.test.d.ts +0 -1
- package/dist/test/integration/tenant-export.test.js +0 -85
- package/dist/test/integration/tenant-export.test.js.map +0 -1
- package/dist/test/unit/attention.test.d.ts +0 -1
- package/dist/test/unit/attention.test.js +0 -109
- package/dist/test/unit/attention.test.js.map +0 -1
- package/dist/test/unit/automation.test.d.ts +0 -1
- package/dist/test/unit/automation.test.js +0 -104
- package/dist/test/unit/automation.test.js.map +0 -1
- package/dist/test/unit/categories.test.d.ts +0 -1
- package/dist/test/unit/categories.test.js +0 -38
- package/dist/test/unit/categories.test.js.map +0 -1
- package/dist/test/unit/engine/conditions.test.d.ts +0 -1
- package/dist/test/unit/engine/conditions.test.js +0 -23
- package/dist/test/unit/engine/conditions.test.js.map +0 -1
- package/dist/test/unit/engine/coverage.test.d.ts +0 -1
- package/dist/test/unit/engine/coverage.test.js +0 -106
- package/dist/test/unit/engine/coverage.test.js.map +0 -1
- package/dist/test/unit/engine/fallback-html.test.d.ts +0 -1
- package/dist/test/unit/engine/fallback-html.test.js +0 -78
- package/dist/test/unit/engine/fallback-html.test.js.map +0 -1
- package/dist/test/unit/engine/paths.test.d.ts +0 -1
- package/dist/test/unit/engine/paths.test.js +0 -33
- package/dist/test/unit/engine/paths.test.js.map +0 -1
- package/dist/test/unit/engine/resolve.test.d.ts +0 -1
- package/dist/test/unit/engine/resolve.test.js +0 -157
- package/dist/test/unit/engine/resolve.test.js.map +0 -1
- package/dist/test/unit/engine/tokens.test.d.ts +0 -1
- package/dist/test/unit/engine/tokens.test.js +0 -26
- package/dist/test/unit/engine/tokens.test.js.map +0 -1
- package/dist/test/unit/events.test.d.ts +0 -1
- package/dist/test/unit/events.test.js +0 -13
- package/dist/test/unit/events.test.js.map +0 -1
- package/dist/test/unit/metrics.test.d.ts +0 -1
- package/dist/test/unit/metrics.test.js +0 -108
- package/dist/test/unit/metrics.test.js.map +0 -1
- package/dist/test/unit/notify.test.d.ts +0 -1
- package/dist/test/unit/notify.test.js +0 -49
- package/dist/test/unit/notify.test.js.map +0 -1
- package/dist/test/unit/period.test.d.ts +0 -1
- package/dist/test/unit/period.test.js +0 -61
- package/dist/test/unit/period.test.js.map +0 -1
- package/dist/test/unit/permissions.test.d.ts +0 -1
- package/dist/test/unit/permissions.test.js +0 -27
- package/dist/test/unit/permissions.test.js.map +0 -1
- package/dist/test/unit/provider.test.d.ts +0 -1
- package/dist/test/unit/provider.test.js +0 -47
- package/dist/test/unit/provider.test.js.map +0 -1
- package/dist/test/unit/realtime.test.d.ts +0 -1
- package/dist/test/unit/realtime.test.js +0 -13
- package/dist/test/unit/realtime.test.js.map +0 -1
- package/dist/test/unit/render.test.d.ts +0 -1
- package/dist/test/unit/render.test.js +0 -23
- package/dist/test/unit/render.test.js.map +0 -1
- package/dist/test/unit/retention.test.d.ts +0 -1
- package/dist/test/unit/retention.test.js +0 -66
- package/dist/test/unit/retention.test.js.map +0 -1
- package/dist/test/unit/send-batch.test.d.ts +0 -1
- package/dist/test/unit/send-batch.test.js +0 -46
- package/dist/test/unit/send-batch.test.js.map +0 -1
- package/dist/test/unit/send.test.d.ts +0 -1
- package/dist/test/unit/send.test.js +0 -120
- package/dist/test/unit/send.test.js.map +0 -1
- package/dist/test/unit/smoke.test.d.ts +0 -1
- package/dist/test/unit/smoke.test.js +0 -10
- package/dist/test/unit/smoke.test.js.map +0 -1
- package/dist/test/unit/status.test.d.ts +0 -1
- package/dist/test/unit/status.test.js +0 -16
- package/dist/test/unit/status.test.js.map +0 -1
- package/dist/test/unit/templates.test.d.ts +0 -1
- package/dist/test/unit/templates.test.js +0 -48
- package/dist/test/unit/templates.test.js.map +0 -1
- package/dist/test/unit/tenant-export.test.d.ts +0 -1
- package/dist/test/unit/tenant-export.test.js +0 -59
- package/dist/test/unit/tenant-export.test.js.map +0 -1
- package/dist/test/unit/webhook-secret.test.d.ts +0 -1
- package/dist/test/unit/webhook-secret.test.js +0 -43
- package/dist/test/unit/webhook-secret.test.js.map +0 -1
- package/frontend-vue/app/components/AttentionCard.vue +0 -78
- package/frontend-vue/app/components/HtmlPreviewModal.vue +0 -20
- package/frontend-vue/app/components/ProviderChip.vue +0 -39
- package/frontend-vue/app/components/editor/EditorBlockSettings.vue +0 -186
- package/frontend-vue/app/components/editor/EditorDataPanel.vue +0 -130
- package/frontend-vue/app/components/editor/EditorDraftBar.vue +0 -35
- package/frontend-vue/app/components/editor/EditorRail.vue +0 -58
- package/frontend-vue/app/components/editor/EditorSubjectSettings.vue +0 -37
- package/frontend-vue/app/components/editor/EditorTemplateSettings.vue +0 -87
- package/frontend-vue/app/components/editor/EditorTestSendModal.vue +0 -66
- package/frontend-vue/app/components/editor/EditorVariables.vue +0 -163
- package/frontend-vue/app/composables/useMailingPeriodData.ts +0 -39
- package/src/antelope-modules.ts +0 -48
- package/src/test/antelope.test.ts +0 -190
- package/src/test/helpers/http.ts +0 -23
- package/src/test/helpers/owner.ts +0 -37
- package/src/test/integration/events.test.ts +0 -159
- package/src/test/integration/retention.test.ts +0 -363
- package/src/test/integration/sends.test.ts +0 -233
- package/src/test/integration/settings.test.ts +0 -128
- package/src/test/integration/setup.test.ts +0 -35
- package/src/test/integration/templates.test.ts +0 -272
- package/src/test/integration/tenant-export.test.ts +0 -125
- package/src/test/unit/attention.test.ts +0 -129
- package/src/test/unit/automation.test.ts +0 -130
- package/src/test/unit/categories.test.ts +0 -51
- package/src/test/unit/engine/conditions.test.ts +0 -35
- package/src/test/unit/engine/coverage.test.ts +0 -118
- package/src/test/unit/engine/fallback-html.test.ts +0 -86
- package/src/test/unit/engine/paths.test.ts +0 -39
- package/src/test/unit/engine/resolve.test.ts +0 -176
- package/src/test/unit/engine/tokens.test.ts +0 -26
- package/src/test/unit/events.test.ts +0 -19
- package/src/test/unit/metrics.test.ts +0 -170
- package/src/test/unit/notify.test.ts +0 -71
- package/src/test/unit/period.test.ts +0 -91
- package/src/test/unit/permissions.test.ts +0 -30
- package/src/test/unit/provider.test.ts +0 -57
- package/src/test/unit/realtime.test.ts +0 -11
- package/src/test/unit/render.test.ts +0 -23
- package/src/test/unit/retention.test.ts +0 -91
- package/src/test/unit/send-batch.test.ts +0 -72
- package/src/test/unit/send.test.ts +0 -161
- package/src/test/unit/smoke.test.ts +0 -8
- package/src/test/unit/status.test.ts +0 -14
- package/src/test/unit/templates.test.ts +0 -52
- package/src/test/unit/tenant-export.test.ts +0 -88
- package/src/test/unit/webhook-secret.test.ts +0 -51
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
TemplateRow,
|
|
3
|
+
TemplateStats,
|
|
4
|
+
TemplateStatus,
|
|
5
|
+
VariableDefinition,
|
|
6
|
+
} from '../types/mailing'
|
|
7
|
+
import { readPath } from './paths'
|
|
8
|
+
|
|
9
|
+
const MS_PER_DAY = 86_400_000
|
|
10
|
+
|
|
11
|
+
export type AttentionReason = 'stale_draft' | 'never_sent' | 'missing_locale'
|
|
12
|
+
|
|
13
|
+
/** How the row's `attention` field separates its reasons. */
|
|
14
|
+
const REASON_SEPARATOR = ','
|
|
15
|
+
|
|
16
|
+
/** Why the server says a template needs a look, read off its row. */
|
|
17
|
+
export function attentionReasons(row: TemplateRow): AttentionReason[] {
|
|
18
|
+
return row.attention
|
|
19
|
+
.split(REASON_SEPARATOR)
|
|
20
|
+
.filter(Boolean) as AttentionReason[]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Whole days between `date` and `now`; 0 for an unreadable date. */
|
|
24
|
+
export function daysSince(date: string | null | undefined, now: Date): number {
|
|
25
|
+
const time = date ? new Date(date).getTime() : Number.NaN
|
|
26
|
+
if (Number.isNaN(time)) return 0
|
|
27
|
+
return Math.max(0, Math.floor((now.getTime() - time) / MS_PER_DAY))
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type CardFooterKind = 'stats' | 'edited' | 'no_send'
|
|
31
|
+
|
|
32
|
+
/** Which line a card's footer reads, with the figures it needs. */
|
|
33
|
+
export interface CardFooter {
|
|
34
|
+
kind: CardFooterKind
|
|
35
|
+
sends: number
|
|
36
|
+
openRate: number
|
|
37
|
+
/** The date the line names: last edit, or the day it went live. */
|
|
38
|
+
at: string
|
|
39
|
+
by: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The footer of a gallery card: the 30-day figures once real sends exist,
|
|
44
|
+
* otherwise who last edited a draft, or since when a live template waits for
|
|
45
|
+
* its first send.
|
|
46
|
+
*/
|
|
47
|
+
export function cardFooter(
|
|
48
|
+
row: TemplateRow,
|
|
49
|
+
stats?: TemplateStats,
|
|
50
|
+
): CardFooter {
|
|
51
|
+
const base = {
|
|
52
|
+
sends: stats?.sends ?? 0,
|
|
53
|
+
openRate: stats?.openRate ?? 0,
|
|
54
|
+
at: row.updatedAt,
|
|
55
|
+
by: row.updatedBy,
|
|
56
|
+
}
|
|
57
|
+
if (base.sends > 0) return { ...base, kind: 'stats' }
|
|
58
|
+
if (row.status === 'live') {
|
|
59
|
+
return { ...base, kind: 'no_send', at: row.publishedAt || row.updatedAt }
|
|
60
|
+
}
|
|
61
|
+
return { ...base, kind: 'edited' }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type LifecycleAction = 'publish' | 'unpublish' | 'archive' | 'restore'
|
|
65
|
+
|
|
66
|
+
type TransitionRule = (row: TemplateRow) => LifecycleAction[]
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The backend refuses any other move with `409 invalid_transition`. On a live
|
|
70
|
+
* template, publishing only makes sense when a draft waits.
|
|
71
|
+
*/
|
|
72
|
+
const TRANSITIONS: Record<TemplateStatus, TransitionRule> = {
|
|
73
|
+
draft: () => ['publish', 'archive'],
|
|
74
|
+
live: (row) =>
|
|
75
|
+
row.isDraftPending
|
|
76
|
+
? ['publish', 'unpublish', 'archive']
|
|
77
|
+
: ['unpublish', 'archive'],
|
|
78
|
+
archived: () => ['restore'],
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The lifecycle moves a template accepts from its current status. */
|
|
82
|
+
export function validTransitions(row: TemplateRow): LifecycleAction[] {
|
|
83
|
+
return TRANSITIONS[row.status]?.(row) ?? []
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const isFilled = (value: unknown) =>
|
|
87
|
+
value !== undefined && value !== null && value !== ''
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Whether the variables are ready for a real send: every path the content
|
|
91
|
+
* uses is declared, and every required one has a test value.
|
|
92
|
+
*/
|
|
93
|
+
export function variablesCovered(
|
|
94
|
+
declared: VariableDefinition[],
|
|
95
|
+
detected: string[],
|
|
96
|
+
testData: Record<string, unknown>,
|
|
97
|
+
): boolean {
|
|
98
|
+
const declaredPaths = new Set(declared.map((entry) => entry.path))
|
|
99
|
+
const everyUsedDeclared = detected.every((path) => declaredPaths.has(path))
|
|
100
|
+
const everyRequiredFilled = declared
|
|
101
|
+
.filter((entry) => entry.required)
|
|
102
|
+
.every((entry) => isFilled(readPath(testData, entry.path)))
|
|
103
|
+
return everyUsedDeclared && everyRequiredFilled
|
|
104
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { SendEventRow, SendStatus } from '../types/mailing'
|
|
1
|
+
import type { SendEventRow, SendRow, SendStatus } from '../types/mailing'
|
|
2
|
+
import { isSuppressedStatus, SUPPRESSED_STATUS } from './suppression'
|
|
2
3
|
|
|
3
4
|
export type TimelineTone =
|
|
4
5
|
'neutral' | 'success' | 'primary' | 'error' | 'warning'
|
|
@@ -13,8 +14,30 @@ export interface TimelineItem {
|
|
|
13
14
|
type: SendStatus
|
|
14
15
|
icon: string
|
|
15
16
|
tone: TimelineTone
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
/** i18n key of the step's title. */
|
|
18
|
+
titleKey: string
|
|
19
|
+
titleParams: Record<string, string>
|
|
20
|
+
/** One line under the title: a raw SMTP answer, a message id, a URL… */
|
|
21
|
+
detail: string
|
|
22
|
+
/** Whether `detail` is a machine value, shown in mono. */
|
|
23
|
+
isDetailMono: boolean
|
|
24
|
+
/** `null` for a step still to come. */
|
|
25
|
+
at: string | null
|
|
26
|
+
/** A step that has not happened yet, drawn ghosted. */
|
|
27
|
+
isPending: boolean
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface TimelineSend extends Pick<
|
|
31
|
+
SendRow,
|
|
32
|
+
'_id' | 'status' | 'createdAt' | 'provider' | 'providerMessageId' | 'error'
|
|
33
|
+
> {
|
|
34
|
+
isSoftBounce?: boolean
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** What the provider reports by itself; `null` while it is unknown. */
|
|
38
|
+
export interface TimelineTracking {
|
|
39
|
+
openTracking: boolean
|
|
40
|
+
clickTracking: boolean
|
|
18
41
|
}
|
|
19
42
|
|
|
20
43
|
export const TIMELINE_META: Record<SendStatus, TimelineMeta> = {
|
|
@@ -27,14 +50,47 @@ export const TIMELINE_META: Record<SendStatus, TimelineMeta> = {
|
|
|
27
50
|
spam: { icon: 'i-ph-prohibit', tone: 'warning' },
|
|
28
51
|
failed: { icon: 'i-ph-warning', tone: 'error' },
|
|
29
52
|
unsubscribed: { icon: 'i-ph-user-minus', tone: 'warning' },
|
|
53
|
+
suppressed: { icon: 'i-ph-prohibit', tone: 'neutral' },
|
|
30
54
|
}
|
|
31
55
|
|
|
32
|
-
|
|
56
|
+
/** The happy path a send walks; pending steps are drawn from it. */
|
|
57
|
+
const HAPPY_PATH: SendStatus[] = ['queued', 'sent', 'delivered', 'opened']
|
|
58
|
+
/** A provider that reports no open stops the path at the delivery. */
|
|
59
|
+
const DELIVERY_PATH: SendStatus[] = ['queued', 'sent', 'delivered']
|
|
60
|
+
/** A provider that reports nothing (SMTP) stops it once the e-mail left. */
|
|
61
|
+
const HANDOFF_PATH: SendStatus[] = ['queued', 'sent']
|
|
62
|
+
|
|
63
|
+
/** Statuses after which nothing more is waited for. */
|
|
64
|
+
const TERMINAL_STATUSES: SendStatus[] = [
|
|
65
|
+
'bounced',
|
|
66
|
+
'spam',
|
|
67
|
+
'failed',
|
|
68
|
+
'unsubscribed',
|
|
69
|
+
SUPPRESSED_STATUS,
|
|
70
|
+
]
|
|
33
71
|
|
|
34
|
-
|
|
35
|
-
|
|
72
|
+
const SOFT_BOUNCE_TYPE = /soft|transient|temporary/i
|
|
73
|
+
const HARD_BOUNCE_TYPE = /hard|permanent/i
|
|
74
|
+
const SOFT_SMTP_ANSWER = /^\s*4\d\d\b|\b4\.\d{1,3}\.\d{1,3}\b/
|
|
75
|
+
|
|
76
|
+
const TITLE_KEY_PREFIX = 'dms_mailing.sends.timeline.'
|
|
77
|
+
|
|
78
|
+
/** Detail keys read off an event, in order; the first present one is shown. */
|
|
79
|
+
const DETAIL_KEYS = ['reason', 'smtp', 'code', 'url', 'userAgent', 'client']
|
|
80
|
+
const MONO_DETAIL_KEYS = new Set(['reason', 'smtp', 'code', 'url'])
|
|
81
|
+
|
|
82
|
+
type EventDetails = Record<string, unknown>
|
|
83
|
+
|
|
84
|
+
interface Detail {
|
|
85
|
+
text: string
|
|
86
|
+
isMono: boolean
|
|
36
87
|
}
|
|
37
88
|
|
|
89
|
+
const NO_DETAIL: Detail = { text: '', isMono: false }
|
|
90
|
+
|
|
91
|
+
/** Steps whose reason the drawer tells in a banner, not as a raw detail. */
|
|
92
|
+
const WORDED_ELSEWHERE = new Set<SendStatus>([SUPPRESSED_STATUS])
|
|
93
|
+
|
|
38
94
|
function parseDetails(raw: string): EventDetails {
|
|
39
95
|
try {
|
|
40
96
|
const parsed: unknown = raw ? JSON.parse(raw) : {}
|
|
@@ -46,23 +102,169 @@ function parseDetails(raw: string): EventDetails {
|
|
|
46
102
|
}
|
|
47
103
|
}
|
|
48
104
|
|
|
49
|
-
function
|
|
50
|
-
const key =
|
|
51
|
-
|
|
105
|
+
function eventDetail(details: EventDetails): Detail {
|
|
106
|
+
const key = DETAIL_KEYS.find((candidate) => details[candidate])
|
|
107
|
+
if (!key) return NO_DETAIL
|
|
108
|
+
return { text: String(details[key]), isMono: MONO_DETAIL_KEYS.has(key) }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** What a step says beyond its event: the message id, the raw error. */
|
|
112
|
+
const SEND_DETAILS: Partial<
|
|
113
|
+
Record<SendStatus, (send: TimelineSend) => Detail>
|
|
114
|
+
> = {
|
|
115
|
+
sent: (send) => ({ text: send.providerMessageId ?? '', isMono: true }),
|
|
116
|
+
bounced: (send) => ({ text: send.error ?? '', isMono: true }),
|
|
117
|
+
failed: (send) => ({ text: send.error ?? '', isMono: true }),
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Steps whose title names the provider when the send knows it. */
|
|
121
|
+
const PROVIDER_TITLE_KEYS: Partial<Record<SendStatus, string>> = {
|
|
122
|
+
sent: `${TITLE_KEY_PREFIX}sent_to`,
|
|
52
123
|
}
|
|
53
124
|
|
|
125
|
+
interface StepTitle {
|
|
126
|
+
titleKey: string
|
|
127
|
+
titleParams: Record<string, string>
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const textOf = (value: unknown): string =>
|
|
131
|
+
typeof value === 'string' ? value : ''
|
|
132
|
+
|
|
54
133
|
/**
|
|
55
|
-
*
|
|
56
|
-
*
|
|
134
|
+
* Whether a bounce event was temporary: the provider's `bounceType` when it
|
|
135
|
+
* gives one, else the SMTP answer's class, else what the send recorded.
|
|
57
136
|
*/
|
|
58
|
-
export function
|
|
137
|
+
export function isSoftBounceEvent(
|
|
138
|
+
details: EventDetails,
|
|
139
|
+
send: TimelineSend,
|
|
140
|
+
): boolean {
|
|
141
|
+
const bounceType = textOf(details.bounceType)
|
|
142
|
+
if (SOFT_BOUNCE_TYPE.test(bounceType)) return true
|
|
143
|
+
if (HARD_BOUNCE_TYPE.test(bounceType)) return false
|
|
144
|
+
const reason = textOf(details.reason)
|
|
145
|
+
return reason ? SOFT_SMTP_ANSWER.test(reason) : !!send.isSoftBounce
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Event titles that depend on what the event says. */
|
|
149
|
+
const EVENT_TITLE_KEYS: Partial<
|
|
150
|
+
Record<SendStatus, (details: EventDetails, send: TimelineSend) => string>
|
|
151
|
+
> = {
|
|
152
|
+
bounced: (details, send) =>
|
|
153
|
+
`${TITLE_KEY_PREFIX}${isSoftBounceEvent(details, send) ? 'soft_bounce' : 'bounced'}`,
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function titleOf(
|
|
157
|
+
type: SendStatus,
|
|
158
|
+
send: TimelineSend,
|
|
159
|
+
details: EventDetails = {},
|
|
160
|
+
): StepTitle {
|
|
161
|
+
const providerKey = PROVIDER_TITLE_KEYS[type]
|
|
162
|
+
if (providerKey && send.provider)
|
|
163
|
+
return { titleKey: providerKey, titleParams: { provider: send.provider } }
|
|
164
|
+
const eventKey = EVENT_TITLE_KEYS[type]?.(details, send)
|
|
165
|
+
return { titleKey: eventKey ?? `${TITLE_KEY_PREFIX}${type}`, titleParams: {} }
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
interface StepInput {
|
|
169
|
+
id: string
|
|
170
|
+
type: SendStatus
|
|
171
|
+
at: string | null
|
|
172
|
+
detail: Detail
|
|
173
|
+
details?: EventDetails
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function stepOf(input: StepInput, send: TimelineSend): TimelineItem {
|
|
177
|
+
return {
|
|
178
|
+
id: input.id,
|
|
179
|
+
type: input.type,
|
|
180
|
+
at: input.at,
|
|
181
|
+
...TIMELINE_META[input.type],
|
|
182
|
+
...titleOf(input.type, send, input.details),
|
|
183
|
+
detail: input.detail.text,
|
|
184
|
+
isDetailMono: input.detail.isMono,
|
|
185
|
+
isPending: input.at === null,
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function happenedSteps(
|
|
190
|
+
events: SendEventRow[],
|
|
191
|
+
send: TimelineSend,
|
|
192
|
+
): TimelineItem[] {
|
|
59
193
|
return [...events]
|
|
60
194
|
.sort((a, b) => new Date(a.at).getTime() - new Date(b.at).getTime())
|
|
61
|
-
.map((event) =>
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
195
|
+
.map((event) => {
|
|
196
|
+
const details = parseDetails(event.json_details)
|
|
197
|
+
const fromEvent = WORDED_ELSEWHERE.has(event.type)
|
|
198
|
+
? NO_DETAIL
|
|
199
|
+
: eventDetail(details)
|
|
200
|
+
const fromSend = SEND_DETAILS[event.type]?.(send) ?? NO_DETAIL
|
|
201
|
+
const detail = fromEvent.text ? fromEvent : fromSend
|
|
202
|
+
return stepOf(
|
|
203
|
+
{ id: event._id, type: event.type, at: event.at, detail, details },
|
|
204
|
+
send,
|
|
205
|
+
)
|
|
206
|
+
})
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** The steps a send can still be seen walking with this provider. */
|
|
210
|
+
function trackedPath(tracking: TimelineTracking | null): SendStatus[] {
|
|
211
|
+
if (!tracking || tracking.openTracking) return HAPPY_PATH
|
|
212
|
+
return tracking.clickTracking ? DELIVERY_PATH : HANDOFF_PATH
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function pendingSteps(
|
|
216
|
+
send: TimelineSend,
|
|
217
|
+
reached: SendStatus[],
|
|
218
|
+
tracking: TimelineTracking | null,
|
|
219
|
+
): TimelineItem[] {
|
|
220
|
+
const isOver = [send.status, ...reached].some((type) =>
|
|
221
|
+
TERMINAL_STATUSES.includes(type),
|
|
222
|
+
)
|
|
223
|
+
if (isOver) return []
|
|
224
|
+
const path = trackedPath(tracking)
|
|
225
|
+
const furthest = Math.max(
|
|
226
|
+
...reached.map((type) => path.indexOf(type as SendStatus)),
|
|
227
|
+
path.indexOf(send.status),
|
|
228
|
+
)
|
|
229
|
+
if (furthest < 0) return []
|
|
230
|
+
return path
|
|
231
|
+
.slice(furthest + 1)
|
|
232
|
+
.map((type) =>
|
|
233
|
+
stepOf(
|
|
234
|
+
{ id: `pending-${type}`, type, at: null, detail: NO_DETAIL },
|
|
235
|
+
send,
|
|
236
|
+
),
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* The steps of a send, oldest first: when it was queued (the send's own date
|
|
242
|
+
* unless an event says so), every recorded event with the detail worth
|
|
243
|
+
* reading (the raw SMTP answer, the provider's message id), then the steps of
|
|
244
|
+
* the happy path still to come, ghosted. A problem or an unsubscription has
|
|
245
|
+
* no step to come, and neither has a step the provider never reports. A send
|
|
246
|
+
* to a suppressed address was never queued: its one step says so.
|
|
247
|
+
*/
|
|
248
|
+
export function buildTimeline(
|
|
249
|
+
events: SendEventRow[],
|
|
250
|
+
send: TimelineSend,
|
|
251
|
+
tracking: TimelineTracking | null = null,
|
|
252
|
+
): TimelineItem[] {
|
|
253
|
+
const happened = happenedSteps(events, send)
|
|
254
|
+
const reached = happened.map((step) => step.type)
|
|
255
|
+
const isQueued = reached.includes('queued') || isSuppressedStatus(send.status)
|
|
256
|
+
const queued = isQueued
|
|
257
|
+
? []
|
|
258
|
+
: [
|
|
259
|
+
stepOf(
|
|
260
|
+
{
|
|
261
|
+
id: `queued-${send._id}`,
|
|
262
|
+
type: 'queued',
|
|
263
|
+
at: send.createdAt,
|
|
264
|
+
detail: NO_DETAIL,
|
|
265
|
+
},
|
|
266
|
+
send,
|
|
267
|
+
),
|
|
268
|
+
]
|
|
269
|
+
return [...queued, ...happened, ...pendingSteps(send, reached, tracking)]
|
|
68
270
|
}
|
|
@@ -1,24 +1,126 @@
|
|
|
1
|
-
|
|
1
|
+
import type { InjectionKey, Ref } from 'vue'
|
|
2
|
+
import { isPresent, readPath } from './paths'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `token` is a `{{path}}` the engine fills in; `invalid` looks like one but
|
|
6
|
+
* breaks its grammar, so the engine leaves it as typed.
|
|
7
|
+
*/
|
|
8
|
+
export type TextPartKind = 'text' | 'token' | 'invalid'
|
|
2
9
|
|
|
3
10
|
export interface TextPart {
|
|
4
11
|
kind: TextPartKind
|
|
5
12
|
value: string
|
|
6
13
|
}
|
|
7
14
|
|
|
8
|
-
|
|
9
|
-
|
|
15
|
+
/** A token read against test data: its value when the data provides one. */
|
|
16
|
+
export interface ResolvedToken {
|
|
17
|
+
isPresent: boolean
|
|
18
|
+
text: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The test data the paper renders tokens with; `null` shows the paths. */
|
|
22
|
+
export type TokenData = Record<string, unknown> | null
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Provided by the canvas in Data mode so every `MailingTokenText` on the paper
|
|
26
|
+
* swaps its tokens for values without each block renderer passing data down.
|
|
27
|
+
*/
|
|
28
|
+
export const TOKEN_DATA_KEY: InjectionKey<Ref<TokenData>> = Symbol(
|
|
29
|
+
'dms-mailing-token-data',
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The engine's token grammar (`src/engine/tokens.ts` `TOKEN_PATTERN`), with
|
|
34
|
+
* a capture around the whole token so `split` keeps it.
|
|
35
|
+
*/
|
|
36
|
+
const TOKEN_SPLIT = /(\{\{\s*[A-Za-z0-9_.[\]]+\s*\}\})/
|
|
37
|
+
const TOKEN_LIKE_SPLIT = /(\{\{[^}]*\}\})/
|
|
10
38
|
const TOKEN_MARKER_LENGTH = 2
|
|
39
|
+
const SPLIT_TOKEN_STEP = 2
|
|
11
40
|
|
|
12
|
-
|
|
41
|
+
/**
|
|
42
|
+
* How a token without a value prints, as the engine's interpolation modes:
|
|
43
|
+
* `preview` keeps it visible, `send` blanks it as a real e-mail does.
|
|
44
|
+
*/
|
|
45
|
+
export type InterpolationMode = 'preview' | 'send'
|
|
46
|
+
|
|
47
|
+
const MISSING_TOKEN: Record<InterpolationMode, (path: string) => string> = {
|
|
48
|
+
preview: (path) => `{{${path}}}`,
|
|
49
|
+
send: () => '',
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** A piece of text with tokens kept exactly as typed (spaces included). */
|
|
53
|
+
export interface RawSegment {
|
|
54
|
+
kind: TextPartKind
|
|
55
|
+
raw: string
|
|
56
|
+
/** The trimmed path of a token; the raw text otherwise. */
|
|
57
|
+
value: string
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const isSplitMatch = (index: number): boolean => index % SPLIT_TOKEN_STEP === 1
|
|
61
|
+
|
|
62
|
+
function literalSegments(text: string): RawSegment[] {
|
|
13
63
|
return text
|
|
14
|
-
.split(
|
|
15
|
-
.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
64
|
+
.split(TOKEN_LIKE_SPLIT)
|
|
65
|
+
.map((raw, index) => ({
|
|
66
|
+
kind: isSplitMatch(index) ? ('invalid' as const) : ('text' as const),
|
|
67
|
+
raw,
|
|
68
|
+
value: raw,
|
|
69
|
+
}))
|
|
70
|
+
.filter((segment) => segment.raw)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Splits `text` into literal text, tokens and token look-alikes, keeping each
|
|
75
|
+
* token's raw spelling: the token field's highlight layer must line up
|
|
76
|
+
* character for character with the textarea.
|
|
77
|
+
*/
|
|
78
|
+
export function splitRawSegments(text: string): RawSegment[] {
|
|
79
|
+
return text.split(TOKEN_SPLIT).flatMap((raw, index) =>
|
|
80
|
+
isSplitMatch(index)
|
|
81
|
+
? [
|
|
82
|
+
{
|
|
19
83
|
kind: 'token' as const,
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
84
|
+
raw,
|
|
85
|
+
value: raw.slice(TOKEN_MARKER_LENGTH, -TOKEN_MARKER_LENGTH).trim(),
|
|
86
|
+
},
|
|
87
|
+
]
|
|
88
|
+
: literalSegments(raw),
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function splitTokens(text: string): TextPart[] {
|
|
93
|
+
return splitRawSegments(text).map(({ kind, value }) => ({ kind, value }))
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function formatValue(value: unknown): string {
|
|
97
|
+
return value instanceof Date ? value.toISOString() : String(value)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Reads one token path in `data`, formatted like the engine prints it. */
|
|
101
|
+
export function resolveToken(
|
|
102
|
+
path: string,
|
|
103
|
+
data: Record<string, unknown>,
|
|
104
|
+
): ResolvedToken {
|
|
105
|
+
const value = readPath(data, path)
|
|
106
|
+
return isPresent(value)
|
|
107
|
+
? { isPresent: true, text: formatValue(value) }
|
|
108
|
+
: { isPresent: false, text: path }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Replaces every token of `text` with its value, as the engine does. */
|
|
112
|
+
export function interpolateText(
|
|
113
|
+
text: string,
|
|
114
|
+
data: Record<string, unknown>,
|
|
115
|
+
mode: InterpolationMode = 'preview',
|
|
116
|
+
): string {
|
|
117
|
+
return splitTokens(text)
|
|
118
|
+
.map((part) => {
|
|
119
|
+
if (part.kind !== 'token') return part.value
|
|
120
|
+
const resolved = resolveToken(part.value, data)
|
|
121
|
+
return resolved.isPresent
|
|
122
|
+
? resolved.text
|
|
123
|
+
: MISSING_TOKEN[mode](part.value)
|
|
124
|
+
})
|
|
125
|
+
.join('')
|
|
24
126
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { VARIABLE_TYPES, type VariableType } from '../types/mailing'
|
|
2
|
+
|
|
3
|
+
const VARIABLE_TYPE_KEY_PREFIX = 'dms_mailing.variable_types'
|
|
4
|
+
|
|
5
|
+
export interface VariableTypeItem {
|
|
6
|
+
value: VariableType
|
|
7
|
+
label: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** The i18n key naming a variable type for a writer ("Amount" for `money`). */
|
|
11
|
+
export function variableTypeLabelKey(type: VariableType): string {
|
|
12
|
+
return `${VARIABLE_TYPE_KEY_PREFIX}.${type}`
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** The type picker's items: the stored value, the label a writer reads. */
|
|
16
|
+
export function variableTypeItems(
|
|
17
|
+
translate: (key: string) => string,
|
|
18
|
+
): VariableTypeItem[] {
|
|
19
|
+
return VARIABLE_TYPES.map((type) => ({
|
|
20
|
+
value: type,
|
|
21
|
+
label: translate(variableTypeLabelKey(type)),
|
|
22
|
+
}))
|
|
23
|
+
}
|