@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,383 @@
|
|
|
1
|
+
import type { MailingTemplate } from "../db";
|
|
2
|
+
import type {
|
|
3
|
+
Block,
|
|
4
|
+
LocaleContent,
|
|
5
|
+
TemplateContent,
|
|
6
|
+
VariableDefinition,
|
|
7
|
+
} from "../types";
|
|
8
|
+
import { footerForCategory } from "../engine";
|
|
9
|
+
import { blankFields, footerBlock } from "./templates";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A ready-made template a new workspace can start from instead of a blank
|
|
13
|
+
* page. It arrives as a draft with its variables declared and test data
|
|
14
|
+
* filled in, so it previews and test-sends right away.
|
|
15
|
+
*/
|
|
16
|
+
export interface StarterTemplate {
|
|
17
|
+
id: string;
|
|
18
|
+
name: string;
|
|
19
|
+
slug: string;
|
|
20
|
+
category: string;
|
|
21
|
+
content: TemplateContent;
|
|
22
|
+
variables: VariableDefinition[];
|
|
23
|
+
/** The test data as an English-speaking workspace gets it. */
|
|
24
|
+
testData: Record<string, unknown>;
|
|
25
|
+
/** The test data with its amounts written the way `locale` writes them. */
|
|
26
|
+
testDataFor: (locale: string) => Record<string, unknown>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type StarterSource = Omit<StarterTemplate, "testData">;
|
|
30
|
+
|
|
31
|
+
const DEFAULT_TEST_DATA_LOCALE = "en";
|
|
32
|
+
const STARTER_CURRENCY = "EUR";
|
|
33
|
+
|
|
34
|
+
/** `locale` when the runtime can format for it, English otherwise. */
|
|
35
|
+
function formattingLocale(locale: string): string {
|
|
36
|
+
try {
|
|
37
|
+
return (
|
|
38
|
+
Intl.NumberFormat.supportedLocalesOf(locale)[0] ??
|
|
39
|
+
DEFAULT_TEST_DATA_LOCALE
|
|
40
|
+
);
|
|
41
|
+
} catch {
|
|
42
|
+
return DEFAULT_TEST_DATA_LOCALE;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The ordered item of the order starter's test data, by language. */
|
|
47
|
+
const ORDER_LINE_NAMES: Record<string, string> = {
|
|
48
|
+
en: "Ergo chair · Graphite × 4",
|
|
49
|
+
fr: "Fauteuil ergonomique · Graphite × 4",
|
|
50
|
+
};
|
|
51
|
+
const LOCALE_PARTS = /[-_]/;
|
|
52
|
+
|
|
53
|
+
/** A test text in the language of `locale`, English when it has none. */
|
|
54
|
+
function inLanguageOf(texts: Record<string, string>, locale: string): string {
|
|
55
|
+
const language = locale.toLowerCase().split(LOCALE_PARTS)[0] ?? "";
|
|
56
|
+
return texts[language] ?? (texts[DEFAULT_TEST_DATA_LOCALE] as string);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** An amount the way `locale` writes it: "€1,556.00", "1 556,00 €". */
|
|
60
|
+
function money(amount: number, locale: string): string {
|
|
61
|
+
return new Intl.NumberFormat(formattingLocale(locale), {
|
|
62
|
+
style: "currency",
|
|
63
|
+
currency: STARTER_CURRENCY,
|
|
64
|
+
}).format(amount);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface StarterCopy {
|
|
68
|
+
subject: string;
|
|
69
|
+
preheader: string;
|
|
70
|
+
heading: string;
|
|
71
|
+
body: string;
|
|
72
|
+
button?: string;
|
|
73
|
+
/** Why the recipient gets this e-mail: the footer's legal line. */
|
|
74
|
+
footer: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const HEADING_SIZE = 26;
|
|
78
|
+
|
|
79
|
+
function localeContent(
|
|
80
|
+
locale: string,
|
|
81
|
+
copy: StarterCopy,
|
|
82
|
+
extra: Block[],
|
|
83
|
+
buttonHref: string,
|
|
84
|
+
): LocaleContent {
|
|
85
|
+
const button: Block[] = copy.button
|
|
86
|
+
? [
|
|
87
|
+
{
|
|
88
|
+
id: "cta",
|
|
89
|
+
type: "button",
|
|
90
|
+
text: copy.button,
|
|
91
|
+
href: buttonHref,
|
|
92
|
+
align: "left",
|
|
93
|
+
visibleIf: null,
|
|
94
|
+
},
|
|
95
|
+
]
|
|
96
|
+
: [];
|
|
97
|
+
return {
|
|
98
|
+
subject: copy.subject,
|
|
99
|
+
preheader: copy.preheader,
|
|
100
|
+
blocks: [
|
|
101
|
+
{
|
|
102
|
+
id: "heading",
|
|
103
|
+
type: "heading",
|
|
104
|
+
text: copy.heading,
|
|
105
|
+
align: "left",
|
|
106
|
+
size: HEADING_SIZE,
|
|
107
|
+
visibleIf: null,
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
id: "intro",
|
|
111
|
+
type: "paragraph",
|
|
112
|
+
text: copy.body,
|
|
113
|
+
align: "left",
|
|
114
|
+
visibleIf: null,
|
|
115
|
+
},
|
|
116
|
+
...extra,
|
|
117
|
+
...button,
|
|
118
|
+
footerBlock(locale, copy.footer),
|
|
119
|
+
],
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const orderLines: Block[] = [
|
|
124
|
+
{
|
|
125
|
+
id: "lines",
|
|
126
|
+
type: "list",
|
|
127
|
+
source: "order.lines",
|
|
128
|
+
labelPath: "name",
|
|
129
|
+
valuePath: "total",
|
|
130
|
+
visibleIf: null,
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
id: "total",
|
|
134
|
+
type: "total",
|
|
135
|
+
label: "Total",
|
|
136
|
+
value: "{{order.total}}",
|
|
137
|
+
visibleIf: null,
|
|
138
|
+
},
|
|
139
|
+
];
|
|
140
|
+
|
|
141
|
+
const v = (
|
|
142
|
+
path: string,
|
|
143
|
+
type: VariableDefinition["type"],
|
|
144
|
+
required = true,
|
|
145
|
+
): VariableDefinition => ({ path, type, required });
|
|
146
|
+
|
|
147
|
+
const ORDER_TOTAL = 1556;
|
|
148
|
+
const INVOICE_AMOUNT = 318.9;
|
|
149
|
+
|
|
150
|
+
const STARTER_SOURCES: StarterSource[] = [
|
|
151
|
+
{
|
|
152
|
+
id: "order-confirmed",
|
|
153
|
+
name: "Order confirmation",
|
|
154
|
+
slug: "order-confirmed",
|
|
155
|
+
category: "orders",
|
|
156
|
+
variables: [
|
|
157
|
+
v("customer.firstName", "string"),
|
|
158
|
+
v("order.number", "string"),
|
|
159
|
+
v("order.total", "money"),
|
|
160
|
+
v("order.lines", "array"),
|
|
161
|
+
v("order.url", "url"),
|
|
162
|
+
],
|
|
163
|
+
testDataFor: (locale) => ({
|
|
164
|
+
customer: { firstName: "Julie" },
|
|
165
|
+
order: {
|
|
166
|
+
number: "#10482",
|
|
167
|
+
total: money(ORDER_TOTAL, locale),
|
|
168
|
+
url: "https://example.com/orders/10482",
|
|
169
|
+
lines: [
|
|
170
|
+
{
|
|
171
|
+
name: inLanguageOf(ORDER_LINE_NAMES, locale),
|
|
172
|
+
total: money(ORDER_TOTAL, locale),
|
|
173
|
+
},
|
|
174
|
+
],
|
|
175
|
+
},
|
|
176
|
+
}),
|
|
177
|
+
content: {
|
|
178
|
+
locales: {
|
|
179
|
+
en: localeContent(
|
|
180
|
+
"en",
|
|
181
|
+
{
|
|
182
|
+
subject: "Thanks for your order {{order.number}}",
|
|
183
|
+
preheader: "We're getting it ready. Here's your summary.",
|
|
184
|
+
heading: "Thanks for your order, {{customer.firstName}}!",
|
|
185
|
+
body: "We've received order {{order.number}} and we're getting it ready.",
|
|
186
|
+
button: "Track your order",
|
|
187
|
+
footer:
|
|
188
|
+
"You are receiving this e-mail because you placed an order with us.",
|
|
189
|
+
},
|
|
190
|
+
orderLines,
|
|
191
|
+
"{{order.url}}",
|
|
192
|
+
),
|
|
193
|
+
fr: localeContent(
|
|
194
|
+
"fr",
|
|
195
|
+
{
|
|
196
|
+
subject: "Merci pour votre commande {{order.number}}",
|
|
197
|
+
preheader: "Nous la préparons. Voici votre récapitulatif.",
|
|
198
|
+
heading: "Merci pour votre commande, {{customer.firstName}} !",
|
|
199
|
+
body: "Nous avons bien reçu la commande {{order.number}} et nous la préparons.",
|
|
200
|
+
button: "Suivre ma commande",
|
|
201
|
+
footer: "Vous recevez cet e-mail suite à votre commande.",
|
|
202
|
+
},
|
|
203
|
+
orderLines,
|
|
204
|
+
"{{order.url}}",
|
|
205
|
+
),
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
id: "order-shipped",
|
|
211
|
+
name: "Shipping update",
|
|
212
|
+
slug: "order-shipped",
|
|
213
|
+
category: "orders",
|
|
214
|
+
variables: [
|
|
215
|
+
v("customer.firstName", "string"),
|
|
216
|
+
v("order.number", "string"),
|
|
217
|
+
v("shipment.trackingUrl", "url"),
|
|
218
|
+
],
|
|
219
|
+
testDataFor: () => ({
|
|
220
|
+
customer: { firstName: "Julie" },
|
|
221
|
+
order: { number: "#10482" },
|
|
222
|
+
shipment: { trackingUrl: "https://example.com/track/10482" },
|
|
223
|
+
}),
|
|
224
|
+
content: {
|
|
225
|
+
locales: {
|
|
226
|
+
en: localeContent(
|
|
227
|
+
"en",
|
|
228
|
+
{
|
|
229
|
+
subject: "Your order {{order.number}} has shipped",
|
|
230
|
+
preheader: "It's on its way.",
|
|
231
|
+
heading: "Your order {{order.number}} has shipped",
|
|
232
|
+
body: "Good news {{customer.firstName}}, your parcel is on its way.",
|
|
233
|
+
button: "Track the parcel",
|
|
234
|
+
footer:
|
|
235
|
+
"You are receiving this e-mail because you placed an order with us.",
|
|
236
|
+
},
|
|
237
|
+
[],
|
|
238
|
+
"{{shipment.trackingUrl}}",
|
|
239
|
+
),
|
|
240
|
+
fr: localeContent(
|
|
241
|
+
"fr",
|
|
242
|
+
{
|
|
243
|
+
subject: "Votre commande {{order.number}} est expédiée",
|
|
244
|
+
preheader: "Elle est en route.",
|
|
245
|
+
heading: "Votre commande {{order.number}} est expédiée",
|
|
246
|
+
body: "Bonne nouvelle {{customer.firstName}}, votre colis est en route.",
|
|
247
|
+
button: "Suivre le colis",
|
|
248
|
+
footer: "Vous recevez cet e-mail suite à votre commande.",
|
|
249
|
+
},
|
|
250
|
+
[],
|
|
251
|
+
"{{shipment.trackingUrl}}",
|
|
252
|
+
),
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
id: "password-reset",
|
|
258
|
+
name: "Password reset",
|
|
259
|
+
slug: "password-reset",
|
|
260
|
+
category: "account",
|
|
261
|
+
variables: [v("user.firstName", "string"), v("reset.url", "url")],
|
|
262
|
+
testDataFor: () => ({
|
|
263
|
+
user: { firstName: "Julie" },
|
|
264
|
+
reset: { url: "https://example.com/reset/abc123" },
|
|
265
|
+
}),
|
|
266
|
+
content: {
|
|
267
|
+
locales: {
|
|
268
|
+
en: localeContent(
|
|
269
|
+
"en",
|
|
270
|
+
{
|
|
271
|
+
subject: "Reset your password",
|
|
272
|
+
preheader: "The link works for one hour.",
|
|
273
|
+
heading: "Reset your password",
|
|
274
|
+
body: "Hi {{user.firstName}}, click the button below to choose a new password. If you didn't ask for it, ignore this e-mail.",
|
|
275
|
+
button: "Choose a new password",
|
|
276
|
+
footer:
|
|
277
|
+
"You are receiving this e-mail because a password reset was requested for your account.",
|
|
278
|
+
},
|
|
279
|
+
[],
|
|
280
|
+
"{{reset.url}}",
|
|
281
|
+
),
|
|
282
|
+
fr: localeContent(
|
|
283
|
+
"fr",
|
|
284
|
+
{
|
|
285
|
+
subject: "Réinitialisez votre mot de passe",
|
|
286
|
+
preheader: "Le lien est valable une heure.",
|
|
287
|
+
heading: "Réinitialisez votre mot de passe",
|
|
288
|
+
body: "Bonjour {{user.firstName}}, cliquez sur le bouton ci-dessous pour choisir un nouveau mot de passe. Si vous ne l'avez pas demandé, ignorez cet e-mail.",
|
|
289
|
+
button: "Choisir un nouveau mot de passe",
|
|
290
|
+
footer:
|
|
291
|
+
"Vous recevez cet e-mail car une réinitialisation du mot de passe a été demandée pour votre compte.",
|
|
292
|
+
},
|
|
293
|
+
[],
|
|
294
|
+
"{{reset.url}}",
|
|
295
|
+
),
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
id: "invoice-ready",
|
|
301
|
+
name: "Invoice available",
|
|
302
|
+
slug: "invoice-ready",
|
|
303
|
+
category: "billing",
|
|
304
|
+
variables: [
|
|
305
|
+
v("customer.firstName", "string"),
|
|
306
|
+
v("invoice.number", "string"),
|
|
307
|
+
v("invoice.amount", "money"),
|
|
308
|
+
v("invoice.url", "url"),
|
|
309
|
+
],
|
|
310
|
+
testDataFor: (locale) => ({
|
|
311
|
+
customer: { firstName: "Julie" },
|
|
312
|
+
invoice: {
|
|
313
|
+
number: "INV-2026-0918",
|
|
314
|
+
amount: money(INVOICE_AMOUNT, locale),
|
|
315
|
+
url: "https://example.com/invoices/0918",
|
|
316
|
+
},
|
|
317
|
+
}),
|
|
318
|
+
content: {
|
|
319
|
+
locales: {
|
|
320
|
+
en: localeContent(
|
|
321
|
+
"en",
|
|
322
|
+
{
|
|
323
|
+
subject: "Invoice {{invoice.number}} is available",
|
|
324
|
+
preheader: "Amount due: {{invoice.amount}}.",
|
|
325
|
+
heading: "Invoice {{invoice.number}} is available",
|
|
326
|
+
body: "Hi {{customer.firstName}}, your invoice of {{invoice.amount}} is ready.",
|
|
327
|
+
button: "Download the invoice",
|
|
328
|
+
footer:
|
|
329
|
+
"You are receiving this e-mail because an invoice was issued for your account.",
|
|
330
|
+
},
|
|
331
|
+
[],
|
|
332
|
+
"{{invoice.url}}",
|
|
333
|
+
),
|
|
334
|
+
fr: localeContent(
|
|
335
|
+
"fr",
|
|
336
|
+
{
|
|
337
|
+
subject: "La facture {{invoice.number}} est disponible",
|
|
338
|
+
preheader: "Montant dû : {{invoice.amount}}.",
|
|
339
|
+
heading: "La facture {{invoice.number}} est disponible",
|
|
340
|
+
body: "Bonjour {{customer.firstName}}, votre facture de {{invoice.amount}} est prête.",
|
|
341
|
+
button: "Télécharger la facture",
|
|
342
|
+
footer:
|
|
343
|
+
"Vous recevez cet e-mail car une facture a été émise pour votre compte.",
|
|
344
|
+
},
|
|
345
|
+
[],
|
|
346
|
+
"{{invoice.url}}",
|
|
347
|
+
),
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
},
|
|
351
|
+
];
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* The starters, each with the footer its category calls for: the shipped ones
|
|
355
|
+
* are transactional, so their footer states why the e-mail was sent and
|
|
356
|
+
* carries no unsubscribe link.
|
|
357
|
+
*/
|
|
358
|
+
export const STARTER_TEMPLATES: StarterTemplate[] = STARTER_SOURCES.map(
|
|
359
|
+
(starter) => ({
|
|
360
|
+
...starter,
|
|
361
|
+
testData: starter.testDataFor(DEFAULT_TEST_DATA_LOCALE),
|
|
362
|
+
content: footerForCategory(starter.content, starter.category),
|
|
363
|
+
}),
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
export function findStarter(id: string): StarterTemplate | undefined {
|
|
367
|
+
return STARTER_TEMPLATES.find((starter) => starter.id === id);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* The content fields a template created from `starter` starts with, its test
|
|
372
|
+
* data written in the workspace's `locale`: its amounts and its texts.
|
|
373
|
+
*/
|
|
374
|
+
export function starterFields(
|
|
375
|
+
starter: StarterTemplate,
|
|
376
|
+
locale: string = DEFAULT_TEST_DATA_LOCALE,
|
|
377
|
+
): Partial<MailingTemplate> {
|
|
378
|
+
return {
|
|
379
|
+
...blankFields(starter.content),
|
|
380
|
+
json_variables: JSON.stringify(starter.variables),
|
|
381
|
+
json_test_data: JSON.stringify(starter.testDataFor(locale)),
|
|
382
|
+
};
|
|
383
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { GetModel } from "@antelopejs/interface-database-decorators";
|
|
2
|
+
import {
|
|
3
|
+
type MailingSuppression,
|
|
4
|
+
normalizeAddress,
|
|
5
|
+
type SendSummary,
|
|
6
|
+
SuppressionModel,
|
|
7
|
+
} from "../db";
|
|
8
|
+
import type {
|
|
9
|
+
SendEventType,
|
|
10
|
+
SuppressionReason,
|
|
11
|
+
SuppressionSummary,
|
|
12
|
+
} from "../types";
|
|
13
|
+
|
|
14
|
+
/** What the event of a send says about its address. */
|
|
15
|
+
export interface SuppressingEvent {
|
|
16
|
+
type: SendEventType;
|
|
17
|
+
isTest: boolean;
|
|
18
|
+
isSoftBounce: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** An address to put on the list. */
|
|
22
|
+
export interface SuppressionEntry {
|
|
23
|
+
email: string;
|
|
24
|
+
reason: SuppressionReason;
|
|
25
|
+
sendId?: string;
|
|
26
|
+
detail?: string;
|
|
27
|
+
createdBy?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const REASON_BY_EVENT: Partial<Record<SendEventType, SuppressionReason>> = {
|
|
31
|
+
bounced: "hard_bounce",
|
|
32
|
+
spam: "complaint",
|
|
33
|
+
unsubscribed: "unsubscribe",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Reasons a test send puts its address on the list too: an address that does
|
|
38
|
+
* not exist does not exist for anyone. A spam report or an unsubscription on
|
|
39
|
+
* a test is the editor's own gesture, not a recipient's choice.
|
|
40
|
+
*/
|
|
41
|
+
const REASONS_FROM_TESTS = new Set<SuppressionReason>(["hard_bounce"]);
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The reason an event puts its address on the suppression list, `null` when
|
|
45
|
+
* it does not: a temporary bounce (a full mailbox) may get through later.
|
|
46
|
+
*/
|
|
47
|
+
export function suppressionReasonOf(
|
|
48
|
+
event: SuppressingEvent,
|
|
49
|
+
): SuppressionReason | null {
|
|
50
|
+
const reason = REASON_BY_EVENT[event.type];
|
|
51
|
+
if (!reason) return null;
|
|
52
|
+
if (event.type === "bounced" && event.isSoftBounce) return null;
|
|
53
|
+
if (event.isTest && !REASONS_FROM_TESTS.has(reason)) return null;
|
|
54
|
+
return reason;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function summarizeSuppression(
|
|
58
|
+
row: MailingSuppression,
|
|
59
|
+
): SuppressionSummary {
|
|
60
|
+
return {
|
|
61
|
+
id: row._id,
|
|
62
|
+
email: row.email,
|
|
63
|
+
reason: row.reason,
|
|
64
|
+
createdAt: row.createdAt,
|
|
65
|
+
sendId: row.sendId || undefined,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Puts an address on the tenant's suppression list; an address already on it
|
|
71
|
+
* keeps its first reason.
|
|
72
|
+
* @returns Whether the address was added
|
|
73
|
+
*/
|
|
74
|
+
export function suppressAddress(
|
|
75
|
+
tenantId: string,
|
|
76
|
+
entry: SuppressionEntry,
|
|
77
|
+
): Promise<boolean> {
|
|
78
|
+
return GetModel(SuppressionModel, tenantId).add(entry);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The suppression entries of the addresses given, by normalized address. */
|
|
82
|
+
export async function suppressionsOf(
|
|
83
|
+
tenantId: string,
|
|
84
|
+
emails: string[],
|
|
85
|
+
): Promise<Map<string, MailingSuppression>> {
|
|
86
|
+
const rows = await GetModel(SuppressionModel, tenantId).listByEmails(emails);
|
|
87
|
+
return new Map(rows.map((row) => [normalizeAddress(row.email), row]));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The ids of the sends whose address is on the suppression list today: a send
|
|
92
|
+
* again of them would not leave.
|
|
93
|
+
*/
|
|
94
|
+
export async function suppressedSendIds(
|
|
95
|
+
tenantId: string,
|
|
96
|
+
sends: Pick<SendSummary, "_id" | "recipientEmail">[],
|
|
97
|
+
): Promise<Set<string>> {
|
|
98
|
+
const entries = await suppressionsOf(
|
|
99
|
+
tenantId,
|
|
100
|
+
sends.map((send) => send.recipientEmail),
|
|
101
|
+
);
|
|
102
|
+
return new Set(
|
|
103
|
+
sends
|
|
104
|
+
.filter((send) => entries.has(normalizeAddress(send.recipientEmail)))
|
|
105
|
+
.map((send) => send._id),
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** The entry of one address, `null` when nothing stops sending to it. */
|
|
110
|
+
export async function findSuppression(
|
|
111
|
+
tenantId: string,
|
|
112
|
+
email: string,
|
|
113
|
+
): Promise<SuppressionSummary | null> {
|
|
114
|
+
const row = await GetModel(SuppressionModel, tenantId).findByEmail(email);
|
|
115
|
+
return row ? summarizeSuppression(row) : null;
|
|
116
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { GetModel } from "@antelopejs/interface-database-decorators";
|
|
2
|
+
import {
|
|
3
|
+
type MailingTemplate,
|
|
4
|
+
SendModel,
|
|
5
|
+
type TemplateAttentionFields,
|
|
6
|
+
TemplateModel,
|
|
7
|
+
} from "../db";
|
|
8
|
+
import { BUSINESS_AUDIENCE, type TemplateAttentionReason } from "../types";
|
|
9
|
+
import {
|
|
10
|
+
knownLocalesOf,
|
|
11
|
+
type TemplateAttentionContext,
|
|
12
|
+
templateAttentionReasons,
|
|
13
|
+
} from "./attention";
|
|
14
|
+
import { forAudience, statsWindowStart } from "./metrics";
|
|
15
|
+
|
|
16
|
+
/** How the stored `attention` field separates its reasons. */
|
|
17
|
+
const REASON_SEPARATOR = ",";
|
|
18
|
+
|
|
19
|
+
/** The attention fields a template gets for its reasons. */
|
|
20
|
+
function attentionFields(
|
|
21
|
+
reasons: TemplateAttentionReason[],
|
|
22
|
+
): TemplateAttentionFields {
|
|
23
|
+
return {
|
|
24
|
+
attention: reasons.join(REASON_SEPARATOR),
|
|
25
|
+
needsAttention: reasons.length > 0,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function attentionContext(
|
|
30
|
+
tenantId: string,
|
|
31
|
+
templates: MailingTemplate[],
|
|
32
|
+
): Promise<TemplateAttentionContext> {
|
|
33
|
+
const now = Date.now();
|
|
34
|
+
const sends = await GetModel(SendModel, tenantId).listBetween(
|
|
35
|
+
statsWindowStart(now),
|
|
36
|
+
new Date(now),
|
|
37
|
+
);
|
|
38
|
+
const real = forAudience(sends, BUSINESS_AUDIENCE);
|
|
39
|
+
return {
|
|
40
|
+
now,
|
|
41
|
+
sentSlugs: new Set(real.map((send) => send.templateSlug)),
|
|
42
|
+
knownLocales: knownLocalesOf(templates),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Works out again why each template of the tenant needs a look, and stores it
|
|
48
|
+
* on the ones whose reasons changed: the templates page filters its "Needs
|
|
49
|
+
* attention" tab on it and the cards show it.
|
|
50
|
+
*/
|
|
51
|
+
export async function refreshTemplateAttention(
|
|
52
|
+
tenantId: string,
|
|
53
|
+
): Promise<void> {
|
|
54
|
+
const model = GetModel(TemplateModel, tenantId);
|
|
55
|
+
const templates = await model.getAll();
|
|
56
|
+
const context = await attentionContext(tenantId, templates);
|
|
57
|
+
const changed = templates
|
|
58
|
+
.map((template) => ({
|
|
59
|
+
template,
|
|
60
|
+
fields: attentionFields(templateAttentionReasons(template, context)),
|
|
61
|
+
}))
|
|
62
|
+
.filter(({ template, fields }) => template.attention !== fields.attention);
|
|
63
|
+
await Promise.all(
|
|
64
|
+
changed.map(({ template, fields }) =>
|
|
65
|
+
model.writeAttention(template._id, fields),
|
|
66
|
+
),
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Refreshes the tenant's attention when `template` was flagged for `reason`,
|
|
72
|
+
* which the write just made may have settled (a stale draft saved, a live
|
|
73
|
+
* template sent).
|
|
74
|
+
*/
|
|
75
|
+
export async function refreshAttentionIfFlagged(
|
|
76
|
+
tenantId: string,
|
|
77
|
+
template: Pick<MailingTemplate, "attention">,
|
|
78
|
+
reason: TemplateAttentionReason,
|
|
79
|
+
): Promise<void> {
|
|
80
|
+
if (template.attention.split(REASON_SEPARATOR).includes(reason))
|
|
81
|
+
await refreshTemplateAttention(tenantId);
|
|
82
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { SendSummary } from "../db";
|
|
2
|
+
import { aggregate, groupBy } from "./metrics";
|
|
3
|
+
import { PROBLEM_STATUSES } from "../types";
|
|
4
|
+
|
|
5
|
+
/** What a template card and the details drawer say about recent sends. */
|
|
6
|
+
export interface TemplateStats {
|
|
7
|
+
id: string;
|
|
8
|
+
slug: string;
|
|
9
|
+
sends: number;
|
|
10
|
+
openRate: number;
|
|
11
|
+
problems: number;
|
|
12
|
+
lastSentAt: string | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface SourceCount {
|
|
16
|
+
source: string;
|
|
17
|
+
count: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Recipients who asked for a locale the template did not carry. */
|
|
21
|
+
export interface FallbackCount {
|
|
22
|
+
requested: string;
|
|
23
|
+
used: string;
|
|
24
|
+
count: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface TemplatePerformance extends TemplateStats {
|
|
28
|
+
sources: SourceCount[];
|
|
29
|
+
fallbacks: FallbackCount[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const UNKNOWN_SOURCE = "";
|
|
33
|
+
const MAX_SOURCES = 5;
|
|
34
|
+
|
|
35
|
+
const isProblem = (send: SendSummary): boolean =>
|
|
36
|
+
PROBLEM_STATUSES.includes(send.status);
|
|
37
|
+
|
|
38
|
+
function lastSentAt(sends: SendSummary[]): string | null {
|
|
39
|
+
const latest = sends.reduce<Date | null>((current, send) => {
|
|
40
|
+
const at = new Date(send.createdAt);
|
|
41
|
+
return !current || at > current ? at : current;
|
|
42
|
+
}, null);
|
|
43
|
+
return latest ? latest.toISOString() : null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function statsOf(
|
|
47
|
+
id: string,
|
|
48
|
+
slug: string,
|
|
49
|
+
sends: SendSummary[],
|
|
50
|
+
): TemplateStats {
|
|
51
|
+
return {
|
|
52
|
+
id,
|
|
53
|
+
slug,
|
|
54
|
+
sends: sends.length,
|
|
55
|
+
openRate: aggregate(sends).openRate ?? 0,
|
|
56
|
+
problems: sends.filter(isProblem).length,
|
|
57
|
+
lastSentAt: lastSentAt(sends),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Sends grouped by the code path that made them, busiest first. */
|
|
62
|
+
export function sourcesOf(sends: SendSummary[]): SourceCount[] {
|
|
63
|
+
return [...groupBy(sends, (send) => send.source ?? UNKNOWN_SOURCE)]
|
|
64
|
+
.map(([source, rows]) => ({ source, count: rows.length }))
|
|
65
|
+
.sort((left, right) => right.count - left.count)
|
|
66
|
+
.slice(0, MAX_SOURCES);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function isFallback(send: SendSummary): boolean {
|
|
70
|
+
return Boolean(send.requestedLocale && send.requestedLocale !== send.locale);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function fallbacksOf(sends: SendSummary[]): FallbackCount[] {
|
|
74
|
+
const fallbacks = sends.filter(isFallback);
|
|
75
|
+
return [
|
|
76
|
+
...groupBy(fallbacks, (send) => `${send.requestedLocale}>${send.locale}`),
|
|
77
|
+
].map(([, rows]) => {
|
|
78
|
+
const first = rows[0] as SendSummary;
|
|
79
|
+
return {
|
|
80
|
+
requested: first.requestedLocale as string,
|
|
81
|
+
used: first.locale,
|
|
82
|
+
count: rows.length,
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function performanceOf(
|
|
88
|
+
id: string,
|
|
89
|
+
slug: string,
|
|
90
|
+
sends: SendSummary[],
|
|
91
|
+
): TemplatePerformance {
|
|
92
|
+
return {
|
|
93
|
+
...statsOf(id, slug, sends),
|
|
94
|
+
sources: sourcesOf(sends),
|
|
95
|
+
fallbacks: fallbacksOf(sends),
|
|
96
|
+
};
|
|
97
|
+
}
|