@antelopejs/dms-mailing 0.3.4 → 0.4.1
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 +115 -41
- 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 +0 -3
- 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 +140 -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 -21
- package/frontend-vue/tests/dnd.test.ts +10 -0
- package/frontend-vue/tests/editor-block-list.test.ts +25 -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 +207 -0
- package/frontend-vue/tests/footer-block.test.ts +89 -0
- package/frontend-vue/tests/funnel-card.test.ts +117 -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 +37 -8
- 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 +211 -0
- package/frontend-vue/tests/overview-gate.test.ts +147 -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 +130 -0
- package/frontend-vue/tests/send-actions.test.ts +128 -0
- package/frontend-vue/tests/send-modals.test.ts +416 -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 +76 -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 +450 -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 +371 -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 +23 -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/frontend-vue/vitest.config.ts +4 -1
- package/package.json +14 -12
- 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 +140 -43
- 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/frontend-vue/tests/smoke.test.ts +0 -8
- 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,181 @@
|
|
|
1
|
+
import type { SendEventRow, SendRow, SendStatus } from '../types/mailing'
|
|
2
|
+
|
|
3
|
+
export type ProblemKind =
|
|
4
|
+
| 'address_missing'
|
|
5
|
+
| 'mailbox_full'
|
|
6
|
+
| 'rejected_spam'
|
|
7
|
+
| 'spam_report'
|
|
8
|
+
| 'provider_timeout'
|
|
9
|
+
| 'provider_unreachable'
|
|
10
|
+
| 'rejected'
|
|
11
|
+
| 'provider_error'
|
|
12
|
+
| 'unsubscribed'
|
|
13
|
+
|
|
14
|
+
export interface ProblemDescription {
|
|
15
|
+
kind: ProblemKind
|
|
16
|
+
icon: string
|
|
17
|
+
/** i18n key of the plain-language title ("This address doesn't exist"). */
|
|
18
|
+
titleKey: string
|
|
19
|
+
/** i18n key of the one-line explanation under it. */
|
|
20
|
+
descriptionKey: string
|
|
21
|
+
params: Record<string, string>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type ProblemSend = Pick<
|
|
25
|
+
SendRow,
|
|
26
|
+
'status' | 'error' | 'recipientEmail' | 'provider'
|
|
27
|
+
>
|
|
28
|
+
|
|
29
|
+
interface ProblemRule {
|
|
30
|
+
kind: ProblemKind
|
|
31
|
+
icon: string
|
|
32
|
+
statuses: SendStatus[]
|
|
33
|
+
/** Matched against the raw error; absent matches any error. */
|
|
34
|
+
pattern?: RegExp
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const KEY_PREFIX = 'dms_mailing.sends.problems.'
|
|
38
|
+
const DOMAIN_SEPARATOR = '@'
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Plain-language readings of a send problem, most specific first: the SMTP
|
|
42
|
+
* class or the provider's wording decides, the status is the fallback.
|
|
43
|
+
*/
|
|
44
|
+
const PROBLEM_RULES: ProblemRule[] = [
|
|
45
|
+
{
|
|
46
|
+
kind: 'address_missing',
|
|
47
|
+
icon: 'i-ph-at',
|
|
48
|
+
statuses: ['bounced', 'failed'],
|
|
49
|
+
pattern:
|
|
50
|
+
/5\.1\.[0-3]|\b550\b|\b553\b|does ?n[o']t exist|unknown (user|recipient)|no such (user|mailbox)|user unknown|invalid (recipient|address)/i,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
kind: 'mailbox_full',
|
|
54
|
+
icon: 'i-ph-tray',
|
|
55
|
+
statuses: ['bounced', 'failed'],
|
|
56
|
+
pattern:
|
|
57
|
+
/\b452\b|\b552\b|[45]\.2\.2|mailbox (is )?full|over quota|quota exceeded/i,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
kind: 'rejected_spam',
|
|
61
|
+
icon: 'i-ph-shield-warning',
|
|
62
|
+
statuses: ['bounced', 'failed'],
|
|
63
|
+
pattern: /5\.7\.[0-9]|\bspam\b|blocked|blacklist|reputation/i,
|
|
64
|
+
},
|
|
65
|
+
{ kind: 'spam_report', icon: 'i-ph-prohibit', statuses: ['spam'] },
|
|
66
|
+
{
|
|
67
|
+
kind: 'provider_timeout',
|
|
68
|
+
icon: 'i-ph-plugs',
|
|
69
|
+
statuses: ['failed'],
|
|
70
|
+
pattern: /time(d)? ?out|ETIMEDOUT|did ?n[o']t answer/i,
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
kind: 'provider_unreachable',
|
|
74
|
+
icon: 'i-ph-plugs',
|
|
75
|
+
statuses: ['failed'],
|
|
76
|
+
pattern:
|
|
77
|
+
/ECONNREFUSED|ECONNRESET|ENOTFOUND|EHOSTUNREACH|ENETUNREACH|EAI_AGAIN|connection refused|unreachable/i,
|
|
78
|
+
},
|
|
79
|
+
{ kind: 'rejected', icon: 'i-ph-arrow-u-up-left', statuses: ['bounced'] },
|
|
80
|
+
{ kind: 'provider_error', icon: 'i-ph-warning', statuses: ['failed'] },
|
|
81
|
+
{ kind: 'unsubscribed', icon: 'i-ph-user-minus', statuses: ['unsubscribed'] },
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
/** Problems the recipient chose rather than suffered: told in amber. */
|
|
85
|
+
const WARNING_KINDS = new Set<ProblemKind>(['spam_report', 'unsubscribed'])
|
|
86
|
+
|
|
87
|
+
/** The banner tone a problem is told in. */
|
|
88
|
+
export function problemTone(kind: ProblemKind): 'warning' | 'error' {
|
|
89
|
+
return WARNING_KINDS.has(kind) ? 'warning' : 'error'
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Problems whose explanation quotes the provider's words; without them, a
|
|
94
|
+
* sentence that stops short of the colon.
|
|
95
|
+
*/
|
|
96
|
+
const KINDS_QUOTING_ERROR = new Set<ProblemKind>(['rejected', 'provider_error'])
|
|
97
|
+
const NO_REASON_SUFFIX = '_no_reason'
|
|
98
|
+
/**
|
|
99
|
+
* Problems whose explanation ends on the advice to send again; when sending
|
|
100
|
+
* again is not possible (the address is on the suppression list), a sentence
|
|
101
|
+
* without that advice.
|
|
102
|
+
*/
|
|
103
|
+
const KINDS_ADVISING_SEND_AGAIN = new Set<ProblemKind>([
|
|
104
|
+
'provider_timeout',
|
|
105
|
+
'provider_unreachable',
|
|
106
|
+
'provider_error',
|
|
107
|
+
])
|
|
108
|
+
const NO_ADVICE_SUFFIX = '_no_advice'
|
|
109
|
+
|
|
110
|
+
function descriptionKey(
|
|
111
|
+
kind: ProblemKind,
|
|
112
|
+
error: string | undefined,
|
|
113
|
+
canSendAgain: boolean,
|
|
114
|
+
): string {
|
|
115
|
+
let key = `${KEY_PREFIX}${kind}.description`
|
|
116
|
+
if (KINDS_QUOTING_ERROR.has(kind) && !error?.trim()) key += NO_REASON_SUFFIX
|
|
117
|
+
if (!canSendAgain && KINDS_ADVISING_SEND_AGAIN.has(kind))
|
|
118
|
+
key += NO_ADVICE_SUFFIX
|
|
119
|
+
return key
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function matches(rule: ProblemRule, send: ProblemSend): boolean {
|
|
123
|
+
if (!rule.statuses.includes(send.status)) return false
|
|
124
|
+
return !rule.pattern || rule.pattern.test(send.error ?? '')
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function domainOf(email: string): string {
|
|
128
|
+
return email.split(DOMAIN_SEPARATOR).at(-1) ?? email
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* What went wrong with a send, in the words of the person reading the log:
|
|
133
|
+
* the title and explanation keys plus the icon of the banner. `null` when the
|
|
134
|
+
* send has no problem. `provider` names the provider when the send does not;
|
|
135
|
+
* `canSendAgain` is false when the address can no longer be sent to.
|
|
136
|
+
*/
|
|
137
|
+
export function describeProblem(
|
|
138
|
+
send: ProblemSend,
|
|
139
|
+
provider = '',
|
|
140
|
+
canSendAgain = true,
|
|
141
|
+
): ProblemDescription | null {
|
|
142
|
+
const rule = PROBLEM_RULES.find((candidate) => matches(candidate, send))
|
|
143
|
+
if (!rule) return null
|
|
144
|
+
return {
|
|
145
|
+
kind: rule.kind,
|
|
146
|
+
icon: rule.icon,
|
|
147
|
+
titleKey: `${KEY_PREFIX}${rule.kind}.title`,
|
|
148
|
+
descriptionKey: descriptionKey(rule.kind, send.error, canSendAgain),
|
|
149
|
+
params: {
|
|
150
|
+
provider: send.provider || provider,
|
|
151
|
+
domain: domainOf(send.recipientEmail),
|
|
152
|
+
error: send.error ?? '',
|
|
153
|
+
},
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const PROBLEM_EVENT_TYPES: SendStatus[] = ['bounced', 'failed', 'spam']
|
|
158
|
+
|
|
159
|
+
interface EventDetails {
|
|
160
|
+
reason?: unknown
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function reasonOf(event: SendEventRow): string {
|
|
164
|
+
try {
|
|
165
|
+
const details = JSON.parse(event.json_details || '{}') as EventDetails
|
|
166
|
+
return typeof details.reason === 'string' ? details.reason : ''
|
|
167
|
+
} catch {
|
|
168
|
+
return ''
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* The provider's words for the latest problem event of a send, for sends
|
|
174
|
+
* logged before their error was kept on the send itself.
|
|
175
|
+
*/
|
|
176
|
+
export function problemEventReason(events: SendEventRow[]): string {
|
|
177
|
+
const problems = events.filter((event) =>
|
|
178
|
+
PROBLEM_EVENT_TYPES.includes(event.type),
|
|
179
|
+
)
|
|
180
|
+
return problems.map(reasonOf).filter(Boolean).at(-1) ?? ''
|
|
181
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { languageLabel } from './locales'
|
|
2
|
+
|
|
3
|
+
/** A manual send reaches at most this many addresses; a list needs an automation. */
|
|
4
|
+
export const MAX_RECIPIENTS = 5
|
|
5
|
+
|
|
6
|
+
const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
|
7
|
+
|
|
8
|
+
export function isEmailAddress(value: string): boolean {
|
|
9
|
+
return EMAIL_PATTERN.test(value.trim())
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Whether a recipient list can be sent: not empty, not over the cap, all addresses. */
|
|
13
|
+
export function isSendableRecipients(recipients: string[]): boolean {
|
|
14
|
+
return (
|
|
15
|
+
recipients.length > 0 &&
|
|
16
|
+
recipients.length <= MAX_RECIPIENTS &&
|
|
17
|
+
recipients.every(isEmailAddress)
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** One locale of a select, labelled with its workspace name when known. */
|
|
22
|
+
export interface LocaleOption {
|
|
23
|
+
label: string
|
|
24
|
+
value: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface NamedLocale {
|
|
28
|
+
code: string
|
|
29
|
+
name?: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Select items for `codes`, named in the interface's language when `uiLocale`
|
|
34
|
+
* is given ("Anglais (EN)" in French), else after the workspace locales.
|
|
35
|
+
*/
|
|
36
|
+
export function localeOptions(
|
|
37
|
+
codes: string[],
|
|
38
|
+
workspace: NamedLocale[],
|
|
39
|
+
uiLocale?: string,
|
|
40
|
+
): LocaleOption[] {
|
|
41
|
+
const names = new Map(workspace.map((entry) => [entry.code, entry.name]))
|
|
42
|
+
return codes.map((code) => {
|
|
43
|
+
const name = names.get(code)
|
|
44
|
+
if (uiLocale)
|
|
45
|
+
return { label: languageLabel(code, uiLocale, name), value: code }
|
|
46
|
+
const upper = code.toUpperCase()
|
|
47
|
+
return { label: name ? `${name} (${upper})` : upper, value: code }
|
|
48
|
+
})
|
|
49
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { SendStatus } from '../types/mailing'
|
|
2
|
+
|
|
3
|
+
const KEY_PREFIX = 'dms_mailing.rendering'
|
|
4
|
+
|
|
5
|
+
/** Statuses that prove the e-mail reached the recipient's mailbox. */
|
|
6
|
+
const RECEIVED_STATUSES: ReadonlySet<SendStatus> = new Set<SendStatus>([
|
|
7
|
+
'delivered',
|
|
8
|
+
'opened',
|
|
9
|
+
'clicked',
|
|
10
|
+
'spam',
|
|
11
|
+
'unsubscribed',
|
|
12
|
+
])
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The i18n key titling a send's rendering: "What {name} received" once the
|
|
16
|
+
* e-mail is known to have arrived, "The e-mail meant for {name}" while it is
|
|
17
|
+
* on its way, bounced, failed or was never sent.
|
|
18
|
+
*/
|
|
19
|
+
export function renderingTitleKey(status: SendStatus): string {
|
|
20
|
+
return RECEIVED_STATUSES.has(status)
|
|
21
|
+
? `${KEY_PREFIX}.received_by`
|
|
22
|
+
: `${KEY_PREFIX}.meant_for`
|
|
23
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import type { SendRow, SendStatus } from '../types/mailing'
|
|
2
|
+
import { isSuppressedStatus } from './suppression'
|
|
3
|
+
|
|
4
|
+
export type SendActionId =
|
|
5
|
+
'rendering' | 'copy_address' | 'fix_address' | 'send_again'
|
|
6
|
+
|
|
7
|
+
export type SendActionVariant = 'ghost' | 'outline' | 'solid'
|
|
8
|
+
|
|
9
|
+
export interface SendAction {
|
|
10
|
+
id: SendActionId
|
|
11
|
+
variant: SendActionVariant
|
|
12
|
+
/** i18n key of the button label; absent for an icon-only button. */
|
|
13
|
+
labelKey?: string
|
|
14
|
+
icon: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A send as the drawer reads it: what the API added to a send since `SendRow`
|
|
19
|
+
* — a temporary bounce, and the link between a send and its send again.
|
|
20
|
+
*/
|
|
21
|
+
export interface DrawerSend extends SendRow {
|
|
22
|
+
isSoftBounce?: boolean
|
|
23
|
+
/** The send this one repeats. */
|
|
24
|
+
replayOf?: string
|
|
25
|
+
/** The send that repeated this one, once someone sent it again. */
|
|
26
|
+
replayedBy?: string
|
|
27
|
+
/** When someone sent it again. */
|
|
28
|
+
resolvedAt?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** What decides a drawer's footer. */
|
|
32
|
+
export interface FooterSend extends Pick<
|
|
33
|
+
DrawerSend,
|
|
34
|
+
'status' | 'isSoftBounce' | 'replayedBy'
|
|
35
|
+
> {
|
|
36
|
+
/**
|
|
37
|
+
* Whether the recipient is on the suppression list today; unknown until
|
|
38
|
+
* the send's detail is loaded.
|
|
39
|
+
*/
|
|
40
|
+
isAddressSuppressed?: boolean
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface FooterRule {
|
|
44
|
+
matches: (send: FooterSend) => boolean
|
|
45
|
+
actions: SendAction[]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const KEY_PREFIX = 'dms_mailing.sends.drawer.actions.'
|
|
49
|
+
const SHORT_ID_LENGTH = 8
|
|
50
|
+
|
|
51
|
+
const RENDERING: SendAction = {
|
|
52
|
+
id: 'rendering',
|
|
53
|
+
variant: 'ghost',
|
|
54
|
+
labelKey: `${KEY_PREFIX}rendering`,
|
|
55
|
+
icon: 'i-ph-eye',
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const COPY_ADDRESS: SendAction = {
|
|
59
|
+
id: 'copy_address',
|
|
60
|
+
variant: 'outline',
|
|
61
|
+
icon: 'i-ph-copy',
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const FIX_ADDRESS: SendAction = {
|
|
65
|
+
id: 'fix_address',
|
|
66
|
+
variant: 'solid',
|
|
67
|
+
labelKey: `${KEY_PREFIX}fix_address`,
|
|
68
|
+
icon: 'i-ph-user-circle',
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const sendAgain = (variant: SendActionVariant): SendAction => ({
|
|
72
|
+
id: 'send_again',
|
|
73
|
+
variant,
|
|
74
|
+
labelKey: `${KEY_PREFIX}send_again`,
|
|
75
|
+
icon: 'i-ph-arrow-clockwise',
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
/** The rendering, one label whatever the status, outlined beside a quiet resend. */
|
|
79
|
+
const RENDERING_OUTLINED: SendAction = { ...RENDERING, variant: 'outline' }
|
|
80
|
+
|
|
81
|
+
/** Footer of a send that went through: repeating it is possible but quiet. */
|
|
82
|
+
const DEFAULT_ACTIONS: SendAction[] = [sendAgain('ghost'), RENDERING_OUTLINED]
|
|
83
|
+
|
|
84
|
+
/** Nothing to send any more: what was received, and the address. */
|
|
85
|
+
const LOOK_ONLY: SendAction[] = [RENDERING, COPY_ADDRESS]
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The drawer footer by status (ML-03): a bounce asks for another address
|
|
89
|
+
* since sending it again would bounce again, a spam report or an
|
|
90
|
+
* unsubscription is never answered with another e-mail, a provider failure is
|
|
91
|
+
* sent again as the main action.
|
|
92
|
+
*/
|
|
93
|
+
const ACTIONS_BY_STATUS: Partial<Record<SendStatus, SendAction[]>> = {
|
|
94
|
+
bounced: [RENDERING, COPY_ADDRESS, FIX_ADDRESS],
|
|
95
|
+
spam: LOOK_ONLY,
|
|
96
|
+
unsubscribed: LOOK_ONLY,
|
|
97
|
+
failed: [RENDERING, sendAgain('solid')],
|
|
98
|
+
suppressed: LOOK_ONLY,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Footers that win over the status: a send someone already sent again offers
|
|
103
|
+
* no second one, a temporary bounce (a full mailbox) may go to the same
|
|
104
|
+
* address later, and so may a suppressed send once its address is
|
|
105
|
+
* reactivated.
|
|
106
|
+
*/
|
|
107
|
+
const FOOTER_RULES: FooterRule[] = [
|
|
108
|
+
{ matches: (send) => !!send.replayedBy, actions: LOOK_ONLY },
|
|
109
|
+
{
|
|
110
|
+
matches: (send) => send.status === 'bounced' && !!send.isSoftBounce,
|
|
111
|
+
actions: [RENDERING, sendAgain('solid')],
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
matches: (send) =>
|
|
115
|
+
isSuppressedStatus(send.status) && send.isAddressSuppressed === false,
|
|
116
|
+
actions: [RENDERING, sendAgain('solid')],
|
|
117
|
+
},
|
|
118
|
+
]
|
|
119
|
+
|
|
120
|
+
const offersSendAgain = (actions: SendAction[]): boolean =>
|
|
121
|
+
actions.some((action) => action.id === 'send_again')
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The footer buttons of a send's drawer, in display order. While the address
|
|
125
|
+
* is on the suppression list a send again would not leave: the footer only
|
|
126
|
+
* looks, and the drawer's notice links to the list.
|
|
127
|
+
*/
|
|
128
|
+
export function sendFooterActions(send: FooterSend): SendAction[] {
|
|
129
|
+
const rule = FOOTER_RULES.find((candidate) => candidate.matches(send))
|
|
130
|
+
const actions =
|
|
131
|
+
rule?.actions ?? ACTIONS_BY_STATUS[send.status] ?? DEFAULT_ACTIONS
|
|
132
|
+
return send.isAddressSuppressed && offersSendAgain(actions)
|
|
133
|
+
? LOOK_ONLY
|
|
134
|
+
: actions
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** The short id the drawer's eyebrow shows ("SEND · 8f2a8c1e"). */
|
|
138
|
+
export function shortSendId(id: string): string {
|
|
139
|
+
return id.slice(-SHORT_ID_LENGTH)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Why a corrected address cannot be used, as an i18n key: empty, not an
|
|
146
|
+
* e-mail, or the address that already failed. `null` when it can.
|
|
147
|
+
*/
|
|
148
|
+
export function correctedAddressError(
|
|
149
|
+
value: string,
|
|
150
|
+
previous: string,
|
|
151
|
+
): string | null {
|
|
152
|
+
const address = value.trim()
|
|
153
|
+
const rules: [boolean, string][] = [
|
|
154
|
+
[!address, 'dms_mailing.sends.fix_address.errors.required'],
|
|
155
|
+
[
|
|
156
|
+
!EMAIL_PATTERN.test(address),
|
|
157
|
+
'dms_mailing.sends.fix_address.errors.invalid',
|
|
158
|
+
],
|
|
159
|
+
[
|
|
160
|
+
address.toLowerCase() === previous.trim().toLowerCase(),
|
|
161
|
+
'dms_mailing.sends.fix_address.errors.same',
|
|
162
|
+
],
|
|
163
|
+
]
|
|
164
|
+
return rules.find(([failed]) => failed)?.[1] ?? null
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* What a send again did: went out, stopped by the suppression list (nothing
|
|
169
|
+
* went out, the original stays a problem), or failed at the provider.
|
|
170
|
+
*/
|
|
171
|
+
export type ReplayOutcome = 'sent' | 'suppressed' | 'failed'
|
|
172
|
+
|
|
173
|
+
export function replayOutcome(result: {
|
|
174
|
+
status: string
|
|
175
|
+
error?: string
|
|
176
|
+
}): ReplayOutcome {
|
|
177
|
+
if (isSuppressedStatus(result.status)) return 'suppressed'
|
|
178
|
+
return result.error || result.status === 'failed' ? 'failed' : 'sent'
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const PAYLOAD_INDENT = 2
|
|
182
|
+
|
|
183
|
+
/** The variables a send carried, pretty-printed; the raw text when unparsable. */
|
|
184
|
+
export function formatPayload(raw: string | undefined): string {
|
|
185
|
+
if (!raw) return ''
|
|
186
|
+
try {
|
|
187
|
+
return JSON.stringify(JSON.parse(raw), null, PAYLOAD_INDENT)
|
|
188
|
+
} catch {
|
|
189
|
+
return raw
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export type SendTone = 'neutral' | 'info' | 'success' | 'warning' | 'error'
|
|
194
|
+
|
|
195
|
+
/** The tone of a send's status pill, the same in the log and the drawer. */
|
|
196
|
+
export const SEND_STATUS_TONES: Record<SendStatus, SendTone> = {
|
|
197
|
+
queued: 'neutral',
|
|
198
|
+
sent: 'info',
|
|
199
|
+
delivered: 'success',
|
|
200
|
+
opened: 'info',
|
|
201
|
+
clicked: 'info',
|
|
202
|
+
bounced: 'error',
|
|
203
|
+
spam: 'warning',
|
|
204
|
+
failed: 'error',
|
|
205
|
+
unsubscribed: 'warning',
|
|
206
|
+
suppressed: 'neutral',
|
|
207
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { SendResult } from '../composables/useMailingApi'
|
|
2
|
+
import { SENDS_PATH } from './pages'
|
|
3
|
+
|
|
4
|
+
/** The DMS table's deep-link key: the send log opens that send's drawer. */
|
|
5
|
+
const RECORD_QUERY_KEY = 'record'
|
|
6
|
+
|
|
7
|
+
export interface SendLogRoute {
|
|
8
|
+
path: string
|
|
9
|
+
query: Record<string, string>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Where a send toast's "View in the send log" leads: the send itself when one
|
|
14
|
+
* went out, the log otherwise.
|
|
15
|
+
*/
|
|
16
|
+
export function sendLogRoute(results: SendResult[]): SendLogRoute {
|
|
17
|
+
const sent = results.filter((result) => !result.error && result.sendId)
|
|
18
|
+
const only = sent.length === 1 ? sent[0] : undefined
|
|
19
|
+
return {
|
|
20
|
+
path: SENDS_PATH,
|
|
21
|
+
query: only ? { [RECORD_QUERY_KEY]: only.sendId } : {},
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** One send of the log, its drawer open. */
|
|
26
|
+
export function sendRoute(sendId: string): SendLogRoute {
|
|
27
|
+
return { path: SENDS_PATH, query: { [RECORD_QUERY_KEY]: sendId } }
|
|
28
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { readPath } from './paths'
|
|
2
|
+
import type { VariableType } from '../types/mailing'
|
|
3
|
+
|
|
4
|
+
export interface SnippetVariable {
|
|
5
|
+
path: string
|
|
6
|
+
type: VariableType
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface SendSnippetInput {
|
|
10
|
+
slug: string
|
|
11
|
+
locale: string
|
|
12
|
+
/**
|
|
13
|
+
* The paths the content uses, declared or only detected, and the system
|
|
14
|
+
* URLs its footer links need (`utils/system-variables.ts`).
|
|
15
|
+
*/
|
|
16
|
+
variables: SnippetVariable[]
|
|
17
|
+
/** The template's test data: example values where it has them. */
|
|
18
|
+
testData: Record<string, unknown>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type VariableTree = Record<string, unknown>
|
|
22
|
+
|
|
23
|
+
const EXAMPLE_VALUES: Record<VariableType, unknown> = {
|
|
24
|
+
string: '…',
|
|
25
|
+
text: '…',
|
|
26
|
+
number: 0,
|
|
27
|
+
money: 0,
|
|
28
|
+
date: '2026-01-31',
|
|
29
|
+
url: 'https://…',
|
|
30
|
+
boolean: true,
|
|
31
|
+
array: [],
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const ARRAY_SUFFIX = '[]'
|
|
35
|
+
const PATH_SEPARATOR = '.'
|
|
36
|
+
const INDENT = ' '
|
|
37
|
+
const JSON_INDENT = 2
|
|
38
|
+
const QUOTED_IDENTIFIER_KEY = /"([A-Za-z_$][\w$]*)":/g
|
|
39
|
+
|
|
40
|
+
const isTree = (value: unknown): value is VariableTree =>
|
|
41
|
+
value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
42
|
+
|
|
43
|
+
/** A list item path (`order.items[].name`) stands for its list. */
|
|
44
|
+
function rootVariable(variable: SnippetVariable): SnippetVariable {
|
|
45
|
+
const at = variable.path.indexOf(ARRAY_SUFFIX)
|
|
46
|
+
return at < 0 ? variable : { path: variable.path.slice(0, at), type: 'array' }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Sets `value` at a dotted path, never over a branch a deeper path built. */
|
|
50
|
+
function setPath(tree: VariableTree, path: string, value: unknown): void {
|
|
51
|
+
const keys = path.split(PATH_SEPARATOR)
|
|
52
|
+
const leaf = keys.pop() as string
|
|
53
|
+
const parent = keys.reduce<VariableTree>((node, key) => {
|
|
54
|
+
if (!isTree(node[key])) node[key] = {}
|
|
55
|
+
return node[key] as VariableTree
|
|
56
|
+
}, tree)
|
|
57
|
+
if (!isTree(parent[leaf])) parent[leaf] = value
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The variables a send passes, as one object of example values. */
|
|
61
|
+
export function exampleVariables(input: SendSnippetInput): VariableTree {
|
|
62
|
+
const tree: VariableTree = {}
|
|
63
|
+
input.variables
|
|
64
|
+
.map(rootVariable)
|
|
65
|
+
.filter((variable) => variable.path)
|
|
66
|
+
.forEach((variable) => {
|
|
67
|
+
const example = readPath(input.testData, variable.path)
|
|
68
|
+
setPath(
|
|
69
|
+
tree,
|
|
70
|
+
variable.path,
|
|
71
|
+
example ?? EXAMPLE_VALUES[variable.type] ?? EXAMPLE_VALUES.string,
|
|
72
|
+
)
|
|
73
|
+
})
|
|
74
|
+
return tree
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function literal(value: unknown): string {
|
|
78
|
+
return JSON.stringify(value, null, JSON_INDENT)
|
|
79
|
+
.replace(QUOTED_IDENTIFIER_KEY, '$1:')
|
|
80
|
+
.split('\n')
|
|
81
|
+
.join(`\n${INDENT}`)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The `SendTemplate` call that sends this template from code: every
|
|
86
|
+
* parameter the interface requires, and the variables its content uses.
|
|
87
|
+
*/
|
|
88
|
+
export function sendSnippet(input: SendSnippetInput): string {
|
|
89
|
+
return [
|
|
90
|
+
`await SendTemplate(${JSON.stringify(input.slug)}, {`,
|
|
91
|
+
`${INDENT}tenantId,`,
|
|
92
|
+
`${INDENT}to: customer.email,`,
|
|
93
|
+
`${INDENT}locale: ${JSON.stringify(input.locale)},`,
|
|
94
|
+
`${INDENT}variables: ${literal(exampleVariables(input))},`,
|
|
95
|
+
'});',
|
|
96
|
+
].join('\n')
|
|
97
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Past this latency the drawer shows the API call in red. */
|
|
2
|
+
export const SLOW_LATENCY_MS = 5000
|
|
3
|
+
|
|
4
|
+
const MS_PER_SECOND = 1000
|
|
5
|
+
const SECONDS_DECIMALS = 1
|
|
6
|
+
|
|
7
|
+
/** A latency as the log prints it: milliseconds, seconds past 5 s, a dash when unknown. */
|
|
8
|
+
export function formatLatency(
|
|
9
|
+
ms: number | null | undefined,
|
|
10
|
+
locale: string,
|
|
11
|
+
): string {
|
|
12
|
+
if (ms === null || ms === undefined) return '—'
|
|
13
|
+
if (ms < SLOW_LATENCY_MS)
|
|
14
|
+
return `${new Intl.NumberFormat(locale).format(ms)} ms`
|
|
15
|
+
const seconds = new Intl.NumberFormat(locale, {
|
|
16
|
+
minimumFractionDigits: SECONDS_DECIMALS,
|
|
17
|
+
maximumFractionDigits: SECONDS_DECIMALS,
|
|
18
|
+
}).format(ms / MS_PER_SECOND)
|
|
19
|
+
return `${seconds} s`
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function isSlowLatency(ms: number | null | undefined): boolean {
|
|
23
|
+
return (ms ?? 0) >= SLOW_LATENCY_MS
|
|
24
|
+
}
|