@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
package/src/engine/coverage.ts
CHANGED
|
@@ -35,7 +35,8 @@ const COLLECTORS: Record<Block["type"], PathCollector> = {
|
|
|
35
35
|
const isUserPath = (path: string): boolean =>
|
|
36
36
|
!SYSTEM_VARIABLE_PATHS.includes(path);
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
/** Every path the content references, the system ones included. */
|
|
39
|
+
export function collectReferencedPaths(content: LocaleContent): string[] {
|
|
39
40
|
const paths = new Set<string>(textTokens(content.subject, content.preheader));
|
|
40
41
|
walkBlocks(content.blocks, (block) => {
|
|
41
42
|
for (const path of COLLECTORS[block.type](block)) {
|
|
@@ -43,7 +44,11 @@ export function collectVariablePaths(content: LocaleContent): string[] {
|
|
|
43
44
|
}
|
|
44
45
|
if (block.visibleIf) paths.add(normalizePath(block.visibleIf.path));
|
|
45
46
|
});
|
|
46
|
-
return [...paths].
|
|
47
|
+
return [...paths].sort();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function collectVariablePaths(content: LocaleContent): string[] {
|
|
51
|
+
return collectReferencedPaths(content).filter(isUserPath);
|
|
47
52
|
}
|
|
48
53
|
|
|
49
54
|
/** Every variable path the content references, across all of its locales. */
|
|
@@ -11,6 +11,12 @@ const ESCAPES: Record<string, string> = {
|
|
|
11
11
|
export const escapeHtml = (value: string): string =>
|
|
12
12
|
value.replace(/[&<>"']/g, (char) => ESCAPES[char] ?? char);
|
|
13
13
|
|
|
14
|
+
const LINE_BREAK = /\r?\n/g;
|
|
15
|
+
|
|
16
|
+
/** Escaped text whose line breaks survive as `<br>`, as typed in the editor. */
|
|
17
|
+
const escapeLines = (value: string): string =>
|
|
18
|
+
escapeHtml(value).replace(LINE_BREAK, "<br>");
|
|
19
|
+
|
|
14
20
|
const SAFE_SCHEMES = ["http:", "https:", "mailto:"];
|
|
15
21
|
const BLOCKED_HREF = "#";
|
|
16
22
|
const SCHEME_PATTERN = /^\s*([a-z][a-z0-9+.-]*):/i;
|
|
@@ -19,56 +25,90 @@ const SCHEME_PATTERN = /^\s*([a-z][a-z0-9+.-]*):/i;
|
|
|
19
25
|
* A link target can be an interpolated variable, so it is data the sender does
|
|
20
26
|
* not control. Escaping the entities keeps it inside the attribute; this keeps
|
|
21
27
|
* the scheme to one a mail client may follow. A target with no scheme is
|
|
22
|
-
* relative (or still an unresolved token) and passes through
|
|
28
|
+
* relative (or still an unresolved token) and passes through; a blocked one
|
|
29
|
+
* becomes `blocked`.
|
|
23
30
|
*/
|
|
24
|
-
export const safeUrl = (
|
|
31
|
+
export const safeUrl = (
|
|
32
|
+
value: string,
|
|
33
|
+
blocked: string = BLOCKED_HREF,
|
|
34
|
+
): string => {
|
|
25
35
|
const scheme = SCHEME_PATTERN.exec(value);
|
|
26
36
|
if (!scheme) return value;
|
|
27
37
|
return SAFE_SCHEMES.includes(scheme[1]!.toLowerCase() + ":")
|
|
28
38
|
? value
|
|
29
|
-
:
|
|
39
|
+
: blocked;
|
|
30
40
|
};
|
|
31
41
|
|
|
42
|
+
/**
|
|
43
|
+
* The font, palette and spacing below are those of the DMS e-mail layout
|
|
44
|
+
* (`emailStyles` of the dms-layout), so an e-mail looks the same whichever
|
|
45
|
+
* renderer drew it.
|
|
46
|
+
*/
|
|
32
47
|
const FONT =
|
|
33
|
-
"font-family:-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif";
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
|
|
48
|
+
"font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif";
|
|
49
|
+
const WRAP = "overflow-wrap:anywhere;word-break:break-word";
|
|
50
|
+
const TEXT_COLOR = "#525252";
|
|
51
|
+
const HEADING_COLOR = "#1a1a1a";
|
|
52
|
+
const BORDER_COLOR = "#e4e4e7";
|
|
53
|
+
const MUTED_COLOR = "#71717a";
|
|
54
|
+
const PAGE_COLOR = "#f6f9fc";
|
|
55
|
+
const CODE_SURFACE = "#f4f4f5";
|
|
56
|
+
const CODE_COLOR = "#18181b";
|
|
57
|
+
const CODE_FONT = "font-family:'SF Mono',Monaco,'Courier New',monospace";
|
|
58
|
+
const TEXT_STYLE = `${FONT};font-size:15px;line-height:1.6;color:${TEXT_COLOR};${WRAP}`;
|
|
59
|
+
const BLOCK_SPACING = "margin:0 0 16px";
|
|
60
|
+
const BUTTON_STYLE = `display:inline-block;padding:12px 24px;border-radius:6px;background:${CODE_COLOR};color:#ffffff;text-decoration:none;font-size:15px;font-weight:600`;
|
|
61
|
+
const DIVIDER = `<hr style="border:0;border-top:1px solid ${BORDER_COLOR};margin:32px 0">`;
|
|
62
|
+
const FOOTER_STYLE = `${FONT};font-size:12px;line-height:1.5;color:${MUTED_COLOR};text-align:center;margin:0;${WRAP}`;
|
|
63
|
+
const FOOTER_LINK_SEPARATOR = " · ";
|
|
41
64
|
const CONTENT_WIDTH = 600;
|
|
65
|
+
const VIEWPORT = "width=device-width, initial-scale=1";
|
|
42
66
|
|
|
43
67
|
type BlockHtml = (block: ResolvedBlock) => string;
|
|
44
68
|
|
|
45
69
|
const link = (label: string, href: string): string =>
|
|
46
70
|
href
|
|
47
|
-
? `<a href="${escapeHtml(safeUrl(href))}" style="color:${
|
|
71
|
+
? `<a href="${escapeHtml(safeUrl(href))}" style="color:${MUTED_COLOR};text-decoration:underline">${escapeHtml(label)}</a>`
|
|
48
72
|
: "";
|
|
49
73
|
|
|
50
|
-
const
|
|
51
|
-
|
|
74
|
+
const row = (label: string, value: string, weight: string): string =>
|
|
75
|
+
`<tr><td style="${TEXT_STYLE};font-weight:${weight}">${escapeHtml(label)}</td><td align="right" style="${TEXT_STYLE};font-weight:${weight};text-align:right">${escapeHtml(value)}</td></tr>`;
|
|
76
|
+
|
|
77
|
+
const table = (rows: string): string =>
|
|
78
|
+
`<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="${BLOCK_SPACING}">${rows}</table>`;
|
|
52
79
|
|
|
53
80
|
const TEXT_RENDERERS: Record<string, BlockHtml> = {
|
|
54
81
|
heading: (block) =>
|
|
55
82
|
block.type === "heading"
|
|
56
|
-
? `<h1 style="${FONT};font-size:${block.size}px;color:${HEADING_COLOR};text-align:${block.align};margin:0 0
|
|
83
|
+
? `<h1 style="${FONT};font-size:${block.size}px;font-weight:600;color:${HEADING_COLOR};text-align:${block.align};margin:0 0 24px;${WRAP}">${escapeLines(block.text)}</h1>`
|
|
57
84
|
: "",
|
|
58
85
|
paragraph: (block) =>
|
|
59
86
|
block.type === "paragraph"
|
|
60
|
-
? `<p style="${
|
|
87
|
+
? `<p style="${TEXT_STYLE};text-align:${block.align};${BLOCK_SPACING}">${escapeLines(block.text)}</p>`
|
|
61
88
|
: "",
|
|
62
89
|
code: (block) =>
|
|
63
90
|
block.type === "code"
|
|
64
|
-
? `<div style="
|
|
91
|
+
? `<div style="background:${CODE_SURFACE};border-radius:6px;margin:24px 0;padding:20px;text-align:center"><p style="${CODE_FONT};font-size:32px;font-weight:700;letter-spacing:6px;color:${CODE_COLOR};margin:0;${WRAP}">${escapeHtml(block.text)}</p></div>`
|
|
65
92
|
: "",
|
|
66
93
|
button: (block) =>
|
|
67
94
|
block.type === "button"
|
|
68
|
-
? `<div style="text-align:${block.align};margin:
|
|
95
|
+
? `<div style="text-align:${block.align};margin:24px 0"><a href="${escapeHtml(safeUrl(block.href))}" style="${FONT};${BUTTON_STYLE}">${escapeHtml(block.text)}</a></div>`
|
|
69
96
|
: "",
|
|
70
97
|
};
|
|
71
98
|
|
|
99
|
+
const footerHtml = (block: ResolvedBlock): string => {
|
|
100
|
+
if (block.type !== "footer") return "";
|
|
101
|
+
const links = [
|
|
102
|
+
link(block.unsubscribeLabel, block.unsubscribeUrl),
|
|
103
|
+
link(block.preferencesLabel, block.preferencesUrl),
|
|
104
|
+
]
|
|
105
|
+
.filter(Boolean)
|
|
106
|
+
.join(FOOTER_LINK_SEPARATOR);
|
|
107
|
+
if (!block.text.trim() && !links) return "";
|
|
108
|
+
const linkLine = links ? `<br>${links}` : "";
|
|
109
|
+
return `${DIVIDER}<p style="${FOOTER_STYLE}">${escapeLines(block.text)}${linkLine}</p>`;
|
|
110
|
+
};
|
|
111
|
+
|
|
72
112
|
const LAYOUT_RENDERERS: Record<string, BlockHtml> = {
|
|
73
113
|
hero: (block) =>
|
|
74
114
|
block.type === "hero" && block.imageUrl
|
|
@@ -76,28 +116,14 @@ const LAYOUT_RENDERERS: Record<string, BlockHtml> = {
|
|
|
76
116
|
: "",
|
|
77
117
|
list: (block) =>
|
|
78
118
|
block.type === "list"
|
|
79
|
-
?
|
|
80
|
-
.map(
|
|
81
|
-
|
|
82
|
-
`<tr><td style="${cellStyle("400", TEXT_COLOR)}">${escapeHtml(row.label)}</td><td align="right" style="${cellStyle("400", HEADING_COLOR)}">${escapeHtml(row.value)}</td></tr>`,
|
|
83
|
-
)
|
|
84
|
-
.join("")}</table>`
|
|
119
|
+
? table(
|
|
120
|
+
block.rows.map((item) => row(item.label, item.value, "400")).join(""),
|
|
121
|
+
)
|
|
85
122
|
: "",
|
|
86
123
|
total: (block) =>
|
|
87
|
-
block.type === "total"
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
divider: () =>
|
|
91
|
-
`<hr style="border:0;border-top:1px solid ${BORDER_COLOR};margin:10px 0 18px">`,
|
|
92
|
-
footer: (block) =>
|
|
93
|
-
block.type === "footer"
|
|
94
|
-
? `<div style="${FONT};font-size:11px;line-height:1.6;color:${MUTED_COLOR};border-top:1px solid ${BORDER_COLOR};padding:16px 0 0">${escapeHtml(block.text)}<br>${[
|
|
95
|
-
link(block.unsubscribeLabel, block.unsubscribeUrl),
|
|
96
|
-
link(block.preferencesLabel, block.preferencesUrl),
|
|
97
|
-
]
|
|
98
|
-
.filter(Boolean)
|
|
99
|
-
.join(" · ")}</div>`
|
|
100
|
-
: "",
|
|
124
|
+
block.type === "total" ? table(row(block.label, block.value, "600")) : "",
|
|
125
|
+
divider: () => DIVIDER,
|
|
126
|
+
footer: footerHtml,
|
|
101
127
|
};
|
|
102
128
|
|
|
103
129
|
const RENDERERS: Record<ResolvedBlock["type"], BlockHtml> = {
|
|
@@ -105,12 +131,24 @@ const RENDERERS: Record<ResolvedBlock["type"], BlockHtml> = {
|
|
|
105
131
|
...LAYOUT_RENDERERS,
|
|
106
132
|
} as Record<ResolvedBlock["type"], BlockHtml>;
|
|
107
133
|
|
|
134
|
+
const FOOTER_TYPE = "footer";
|
|
135
|
+
|
|
136
|
+
const renderBlocks = (blocks: ResolvedBlock[]): string =>
|
|
137
|
+
blocks.map((block) => RENDERERS[block.type](block)).join("");
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The e-mail without the DMS front end: the same blocks, in the same order and
|
|
141
|
+
* look as the Vue template, the footer last, under its divider.
|
|
142
|
+
*/
|
|
108
143
|
export function renderFallbackHtml(
|
|
109
144
|
email: ResolvedEmail,
|
|
110
145
|
locale: string,
|
|
111
146
|
): string {
|
|
112
|
-
const body =
|
|
113
|
-
.
|
|
114
|
-
|
|
115
|
-
|
|
147
|
+
const body = renderBlocks(
|
|
148
|
+
email.blocks.filter((block) => block.type !== FOOTER_TYPE),
|
|
149
|
+
);
|
|
150
|
+
const footer = renderBlocks(
|
|
151
|
+
email.blocks.filter((block) => block.type === FOOTER_TYPE),
|
|
152
|
+
);
|
|
153
|
+
return `<!DOCTYPE html><html lang="${escapeHtml(locale)}"><head><meta charset="utf-8"><meta name="viewport" content="${VIEWPORT}"><title>${escapeHtml(email.subject)}</title></head><body style="margin:0;background:${PAGE_COLOR};${FONT}"><span style="display:none;max-height:0;overflow:hidden">${escapeHtml(email.preheader)}</span><table role="presentation" width="100%"><tr><td align="center" style="padding:20px 0 48px"><table role="presentation" width="${CONTENT_WIDTH}" style="max-width:${CONTENT_WIDTH}px;width:100%;background:#ffffff;border-radius:8px;box-shadow:0 1px 3px rgba(0,0,0,0.1)"><tr><td style="padding:40px">${body}${footer}</td></tr></table></td></tr></table></body></html>`;
|
|
116
154
|
}
|
package/src/engine/index.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
export * from "./changes";
|
|
1
2
|
export * from "./conditions";
|
|
2
3
|
export * from "./coverage";
|
|
3
4
|
export * from "./fallback-html";
|
|
4
5
|
export * from "./paths";
|
|
6
|
+
export * from "./plain-text";
|
|
5
7
|
export * from "./resolve";
|
|
8
|
+
export * from "./system-variables";
|
|
6
9
|
export * from "./tokens";
|
|
7
10
|
export * from "./walk";
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { ResolvedBlock, ResolvedEmail } from "../types";
|
|
2
|
+
|
|
3
|
+
type BlockText = (block: ResolvedBlock, separator: string) => string;
|
|
4
|
+
|
|
5
|
+
const PARAGRAPH_BREAK = "\n\n";
|
|
6
|
+
const LINE_BREAK = "\n";
|
|
7
|
+
const DEFAULT_LABEL_SEPARATOR = ": ";
|
|
8
|
+
const LOCALE_PARTS = /[-_]/;
|
|
9
|
+
const DIVIDER = "----------";
|
|
10
|
+
const FOOTER_TYPE = "footer";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Languages whose typography spaces a colon off its label, with a no-break
|
|
14
|
+
* space so the colon never starts a line ("Total : 64,00 €").
|
|
15
|
+
*/
|
|
16
|
+
const LABEL_SEPARATORS: Record<string, string> = {
|
|
17
|
+
fr: " : ",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** What goes between a label and its value in the e-mail's language. */
|
|
21
|
+
export function labelSeparatorOf(locale = ""): string {
|
|
22
|
+
const language = locale.toLowerCase().split(LOCALE_PARTS)[0] ?? "";
|
|
23
|
+
return LABEL_SEPARATORS[language] ?? DEFAULT_LABEL_SEPARATOR;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const linked = (label: string, href: string, separator: string): string =>
|
|
27
|
+
href ? `${label}${separator}${href}` : "";
|
|
28
|
+
|
|
29
|
+
const rowsText = (rows: string[][], separator: string): string =>
|
|
30
|
+
rows
|
|
31
|
+
.map((cells) => cells.filter(Boolean).join(separator))
|
|
32
|
+
.filter(Boolean)
|
|
33
|
+
.join(LINE_BREAK);
|
|
34
|
+
|
|
35
|
+
const footerText = (block: ResolvedBlock, separator: string): string => {
|
|
36
|
+
if (block.type !== FOOTER_TYPE) return "";
|
|
37
|
+
const lines = [
|
|
38
|
+
block.text,
|
|
39
|
+
linked(block.unsubscribeLabel, block.unsubscribeUrl, separator),
|
|
40
|
+
linked(block.preferencesLabel, block.preferencesUrl, separator),
|
|
41
|
+
].filter((line) => line.trim());
|
|
42
|
+
return lines.length ? [DIVIDER, ...lines].join(LINE_BREAK) : "";
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const BLOCK_TEXT: Record<ResolvedBlock["type"], BlockText> = {
|
|
46
|
+
hero: (block) => (block.type === "hero" && block.imageUrl ? block.alt : ""),
|
|
47
|
+
heading: (block) => (block.type === "heading" ? block.text : ""),
|
|
48
|
+
paragraph: (block) => (block.type === "paragraph" ? block.text : ""),
|
|
49
|
+
code: (block) => (block.type === "code" ? block.text : ""),
|
|
50
|
+
list: (block, separator) =>
|
|
51
|
+
block.type === "list"
|
|
52
|
+
? rowsText(
|
|
53
|
+
block.rows.map((row) => [row.label, row.value]),
|
|
54
|
+
separator,
|
|
55
|
+
)
|
|
56
|
+
: "",
|
|
57
|
+
total: (block, separator) =>
|
|
58
|
+
block.type === "total"
|
|
59
|
+
? rowsText([[block.label, block.value]], separator)
|
|
60
|
+
: "",
|
|
61
|
+
button: (block, separator) =>
|
|
62
|
+
block.type === "button" ? linked(block.text, block.href, separator) : "",
|
|
63
|
+
divider: () => DIVIDER,
|
|
64
|
+
footer: footerText,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const blocksText = (blocks: ResolvedBlock[], separator: string): string[] =>
|
|
68
|
+
blocks
|
|
69
|
+
.map((block) => BLOCK_TEXT[block.type](block, separator).trim())
|
|
70
|
+
.filter(Boolean);
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The plain-text part of the e-mail: the same blocks as the HTML, in the same
|
|
74
|
+
* order, footer last, punctuated the way the e-mail's language writes it.
|
|
75
|
+
* Mail clients that show no HTML, and spam filters that weigh a missing text
|
|
76
|
+
* part, read this one.
|
|
77
|
+
*/
|
|
78
|
+
export function renderPlainText(email: ResolvedEmail, locale = ""): string {
|
|
79
|
+
const separator = labelSeparatorOf(locale);
|
|
80
|
+
const body = email.blocks.filter((block) => block.type !== FOOTER_TYPE);
|
|
81
|
+
const footer = email.blocks.filter((block) => block.type === FOOTER_TYPE);
|
|
82
|
+
return [
|
|
83
|
+
...blocksText(body, separator),
|
|
84
|
+
...blocksText(footer, separator),
|
|
85
|
+
].join(PARAGRAPH_BREAK);
|
|
86
|
+
}
|
package/src/engine/resolve.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
PREFERENCES_URL_PATH,
|
|
3
|
+
SYSTEM_VARIABLE_PATHS,
|
|
4
|
+
UNSUBSCRIBE_URL_PATH,
|
|
5
|
+
} from "../constants";
|
|
2
6
|
import type {
|
|
3
7
|
Block,
|
|
8
|
+
FooterBlock,
|
|
4
9
|
IfBlock,
|
|
5
10
|
ListBlock,
|
|
6
11
|
LocaleContent,
|
|
@@ -9,28 +14,51 @@ import type {
|
|
|
9
14
|
ResolvedListRow,
|
|
10
15
|
} from "../types";
|
|
11
16
|
import { evaluateCondition } from "./conditions";
|
|
17
|
+
import { safeUrl } from "./fallback-html";
|
|
12
18
|
import { getPath, isPresent } from "./paths";
|
|
13
|
-
import {
|
|
19
|
+
import { footerLinkPaths } from "./system-variables";
|
|
20
|
+
import { type InterpolationMode, interpolate } from "./tokens";
|
|
14
21
|
|
|
15
22
|
type Data = Record<string, unknown>;
|
|
16
23
|
|
|
17
24
|
interface ResolveState {
|
|
18
25
|
data: Data;
|
|
26
|
+
mode: InterpolationMode;
|
|
19
27
|
missing: Set<string>;
|
|
20
28
|
hidden: string[];
|
|
21
29
|
}
|
|
22
30
|
|
|
31
|
+
/** A blocked image source: the hero is left out rather than drawn broken. */
|
|
32
|
+
const BLOCKED_IMAGE = "";
|
|
33
|
+
|
|
23
34
|
type BlockResolver = (block: Block, state: ResolveState) => ResolvedBlock[];
|
|
24
35
|
|
|
25
36
|
function text(state: ResolveState, value: string): string {
|
|
26
|
-
const result = interpolate(value, state.data);
|
|
37
|
+
const result = interpolate(value, state.data, state.mode);
|
|
27
38
|
for (const path of result.missing) state.missing.add(path);
|
|
28
39
|
return result.text;
|
|
29
40
|
}
|
|
30
41
|
|
|
31
|
-
|
|
42
|
+
/**
|
|
43
|
+
* A link or image target, filtered here so both renderers (the Vue e-mail
|
|
44
|
+
* and the fallback) receive a scheme a mail client may follow.
|
|
45
|
+
*/
|
|
46
|
+
function url(state: ResolveState, value: string, blocked?: string): string {
|
|
47
|
+
return safeUrl(text(state, value), blocked);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A footer link's target, read from the data the caller passed. A link whose
|
|
52
|
+
* label was cleared is left out, whatever URL the caller gave.
|
|
53
|
+
*/
|
|
54
|
+
function systemUrl(
|
|
55
|
+
state: ResolveState,
|
|
56
|
+
block: FooterBlock,
|
|
57
|
+
path: string,
|
|
58
|
+
): string {
|
|
59
|
+
if (!footerLinkPaths(block).includes(path)) return "";
|
|
32
60
|
const value = getPath(state.data, path);
|
|
33
|
-
return isPresent(value) ? String(value) : "";
|
|
61
|
+
return isPresent(value) ? safeUrl(String(value)) : "";
|
|
34
62
|
}
|
|
35
63
|
|
|
36
64
|
function cell(item: unknown, path: string): string {
|
|
@@ -65,7 +93,7 @@ const RESOLVERS: Record<Block["type"], BlockResolver> = {
|
|
|
65
93
|
{
|
|
66
94
|
id: block.id,
|
|
67
95
|
type: "hero",
|
|
68
|
-
imageUrl:
|
|
96
|
+
imageUrl: url(state, block.imageUrl, BLOCKED_IMAGE),
|
|
69
97
|
alt: text(state, block.alt),
|
|
70
98
|
},
|
|
71
99
|
]
|
|
@@ -119,7 +147,7 @@ const RESOLVERS: Record<Block["type"], BlockResolver> = {
|
|
|
119
147
|
id: block.id,
|
|
120
148
|
type: "button",
|
|
121
149
|
text: text(state, block.text),
|
|
122
|
-
href:
|
|
150
|
+
href: url(state, block.href),
|
|
123
151
|
align: block.align,
|
|
124
152
|
},
|
|
125
153
|
]
|
|
@@ -135,20 +163,21 @@ const RESOLVERS: Record<Block["type"], BlockResolver> = {
|
|
|
135
163
|
text: text(state, block.text),
|
|
136
164
|
unsubscribeLabel: block.unsubscribeLabel,
|
|
137
165
|
preferencesLabel: block.preferencesLabel,
|
|
138
|
-
unsubscribeUrl: systemUrl(state,
|
|
139
|
-
preferencesUrl: systemUrl(state,
|
|
166
|
+
unsubscribeUrl: systemUrl(state, block, UNSUBSCRIBE_URL_PATH),
|
|
167
|
+
preferencesUrl: systemUrl(state, block, PREFERENCES_URL_PATH),
|
|
140
168
|
},
|
|
141
169
|
]
|
|
142
170
|
: [],
|
|
143
171
|
};
|
|
144
172
|
|
|
173
|
+
/**
|
|
174
|
+
* Visibility decides before a block is resolved, not after: resolving first
|
|
175
|
+
* would record a hidden block's unresolved paths in `state.missing`, and
|
|
176
|
+
* `blockOnMissingVariables` refuses a send on that set, so a hidden block
|
|
177
|
+
* could block a send over a variable it never rendered.
|
|
178
|
+
*/
|
|
145
179
|
function resolveBlocks(blocks: Block[], state: ResolveState): ResolvedBlock[] {
|
|
146
180
|
return blocks.flatMap((block) => {
|
|
147
|
-
// Visibility decides before the block is resolved, not after. Resolving
|
|
148
|
-
// first recorded the block's unresolved paths in `state.missing` even
|
|
149
|
-
// though it was then discarded — and `blockOnMissingVariables` refuses a
|
|
150
|
-
// send on that set, so a hidden block could block a send over a variable
|
|
151
|
-
// it never rendered.
|
|
152
181
|
if (block.visibleIf && !evaluateCondition(block.visibleIf, state.data)) {
|
|
153
182
|
state.hidden.push(block.id);
|
|
154
183
|
return [];
|
|
@@ -160,11 +189,16 @@ function resolveBlocks(blocks: Block[], state: ResolveState): ResolvedBlock[] {
|
|
|
160
189
|
const isUserPath = (path: string): boolean =>
|
|
161
190
|
!SYSTEM_VARIABLE_PATHS.includes(path);
|
|
162
191
|
|
|
192
|
+
/**
|
|
193
|
+
* Resolves `content` against `data`. A real send passes `send` so a missing
|
|
194
|
+
* value leaves a blank; previews keep the token visible.
|
|
195
|
+
*/
|
|
163
196
|
export function resolveEmail(
|
|
164
197
|
content: LocaleContent,
|
|
165
198
|
data: Data,
|
|
199
|
+
mode: InterpolationMode = "preview",
|
|
166
200
|
): ResolvedEmail {
|
|
167
|
-
const state: ResolveState = { data, missing: new Set(), hidden: [] };
|
|
201
|
+
const state: ResolveState = { data, mode, missing: new Set(), hidden: [] };
|
|
168
202
|
const subject = text(state, content.subject);
|
|
169
203
|
const preheader = text(state, content.preheader);
|
|
170
204
|
const blocks = resolveBlocks(content.blocks, state);
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PREFERENCES_URL_PATH,
|
|
3
|
+
SYSTEM_VARIABLE_PATHS,
|
|
4
|
+
UNSUBSCRIBE_URL_PATH,
|
|
5
|
+
} from "../constants";
|
|
6
|
+
import type {
|
|
7
|
+
Block,
|
|
8
|
+
FooterBlock,
|
|
9
|
+
LocaleContent,
|
|
10
|
+
TemplateContent,
|
|
11
|
+
VariableDefinition,
|
|
12
|
+
} from "../types";
|
|
13
|
+
import { collectReferencedPaths } from "./coverage";
|
|
14
|
+
import { getPath, isPresent } from "./paths";
|
|
15
|
+
import { walkBlocks } from "./walk";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The category whose e-mails are campaigns; every other one is transactional.
|
|
19
|
+
* The settings keep it: it can be renamed, never removed.
|
|
20
|
+
*/
|
|
21
|
+
export const MARKETING_CATEGORY = "marketing";
|
|
22
|
+
|
|
23
|
+
const SYSTEM_VARIABLE_TYPE = "url";
|
|
24
|
+
|
|
25
|
+
type FooterLabelKey = "unsubscribeLabel" | "preferencesLabel";
|
|
26
|
+
|
|
27
|
+
/** Each footer link: the label that shows it and the URL the caller passes. */
|
|
28
|
+
const FOOTER_LINKS: Record<FooterLabelKey, string> = {
|
|
29
|
+
unsubscribeLabel: UNSUBSCRIBE_URL_PATH,
|
|
30
|
+
preferencesLabel: PREFERENCES_URL_PATH,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const LABEL_KEYS = Object.keys(FOOTER_LINKS) as FooterLabelKey[];
|
|
34
|
+
|
|
35
|
+
export function isMarketingCategory(category?: string | null): boolean {
|
|
36
|
+
return category === MARKETING_CATEGORY;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Whether a new footer of this category carries the unsubscribe and
|
|
41
|
+
* preferences links: a transactional e-mail (an order, a password reset…)
|
|
42
|
+
* is not subscribed to, so only an uncategorised or marketing one does.
|
|
43
|
+
*/
|
|
44
|
+
export function hasFooterLinksByDefault(category?: string | null): boolean {
|
|
45
|
+
return !category || isMarketingCategory(category);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const isFooter = (block: Block): block is FooterBlock =>
|
|
49
|
+
block.type === "footer";
|
|
50
|
+
|
|
51
|
+
/** The URL paths a footer shows a link for: the ones with a label. */
|
|
52
|
+
export function footerLinkPaths(block: FooterBlock): string[] {
|
|
53
|
+
return LABEL_KEYS.filter((key) => block[key].trim()).map(
|
|
54
|
+
(key) => FOOTER_LINKS[key],
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The system paths a locale uses: footer links, then `{{…}}` references. */
|
|
59
|
+
export function localeSystemPaths(content: LocaleContent): string[] {
|
|
60
|
+
const paths = new Set<string>();
|
|
61
|
+
walkBlocks(content.blocks, (block) => {
|
|
62
|
+
if (isFooter(block)) footerLinkPaths(block).forEach((p) => paths.add(p));
|
|
63
|
+
});
|
|
64
|
+
collectReferencedPaths(content)
|
|
65
|
+
.filter((path) => SYSTEM_VARIABLE_PATHS.includes(path))
|
|
66
|
+
.forEach((path) => paths.add(path));
|
|
67
|
+
return SYSTEM_VARIABLE_PATHS.filter((path) => paths.has(path));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The system paths any locale of the content uses. */
|
|
71
|
+
export function contentSystemPaths(content: TemplateContent): string[] {
|
|
72
|
+
const used = new Set(
|
|
73
|
+
Object.values(content.locales).flatMap(localeSystemPaths),
|
|
74
|
+
);
|
|
75
|
+
return SYSTEM_VARIABLE_PATHS.filter((path) => used.has(path));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The URLs a template's caller passes for the links its content shows,
|
|
80
|
+
* declared like any variable of its contract: a marketing e-mail must carry a
|
|
81
|
+
* working unsubscribe link, so they are required there.
|
|
82
|
+
*/
|
|
83
|
+
export function systemVariables(
|
|
84
|
+
paths: string[],
|
|
85
|
+
category?: string | null,
|
|
86
|
+
): VariableDefinition[] {
|
|
87
|
+
const required = isMarketingCategory(category);
|
|
88
|
+
return paths.map((path) => ({ path, type: SYSTEM_VARIABLE_TYPE, required }));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The required system URLs `data` does not provide. */
|
|
92
|
+
export function missingSystemVariables(
|
|
93
|
+
content: LocaleContent,
|
|
94
|
+
category: string | null | undefined,
|
|
95
|
+
data: Record<string, unknown>,
|
|
96
|
+
): string[] {
|
|
97
|
+
return systemVariables(localeSystemPaths(content), category)
|
|
98
|
+
.filter((variable) => variable.required)
|
|
99
|
+
.map((variable) => variable.path)
|
|
100
|
+
.filter((path) => !isPresent(getPath(data, path)));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const withoutLinks = (block: Block): Block =>
|
|
104
|
+
isFooter(block)
|
|
105
|
+
? { ...block, unsubscribeLabel: "", preferencesLabel: "" }
|
|
106
|
+
: block;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The content with the footers its category calls for: a transactional
|
|
110
|
+
* template loses the unsubscribe and preferences links.
|
|
111
|
+
*/
|
|
112
|
+
export function footerForCategory(
|
|
113
|
+
content: TemplateContent,
|
|
114
|
+
category?: string | null,
|
|
115
|
+
): TemplateContent {
|
|
116
|
+
if (hasFooterLinksByDefault(category)) return content;
|
|
117
|
+
const locales = Object.fromEntries(
|
|
118
|
+
Object.entries(content.locales).map(([code, locale]) => [
|
|
119
|
+
code,
|
|
120
|
+
{ ...locale, blocks: locale.blocks.map(withoutLinks) },
|
|
121
|
+
]),
|
|
122
|
+
);
|
|
123
|
+
return { ...content, locales };
|
|
124
|
+
}
|
package/src/engine/tokens.ts
CHANGED
|
@@ -2,11 +2,22 @@ import { getPath, isPresent } from "./paths";
|
|
|
2
2
|
|
|
3
3
|
export const TOKEN_PATTERN = /\{\{\s*([A-Za-z0-9_.[\]]+)\s*\}\}/g;
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* How a token without a value prints. `preview` keeps it visible so an author
|
|
7
|
+
* sees what is missing; `send` blanks it, so a recipient never reads braces.
|
|
8
|
+
*/
|
|
9
|
+
export type InterpolationMode = "preview" | "send";
|
|
10
|
+
|
|
5
11
|
export interface InterpolationResult {
|
|
6
12
|
text: string;
|
|
7
13
|
missing: string[];
|
|
8
14
|
}
|
|
9
15
|
|
|
16
|
+
const MISSING_TOKEN: Record<InterpolationMode, (token: string) => string> = {
|
|
17
|
+
preview: (token) => token,
|
|
18
|
+
send: () => "",
|
|
19
|
+
};
|
|
20
|
+
|
|
10
21
|
export function extractTokens(text: string): string[] {
|
|
11
22
|
const found = new Set<string>();
|
|
12
23
|
for (const match of text.matchAll(TOKEN_PATTERN))
|
|
@@ -21,13 +32,14 @@ function formatValue(value: unknown): string {
|
|
|
21
32
|
export function interpolate(
|
|
22
33
|
text: string,
|
|
23
34
|
data: Record<string, unknown>,
|
|
35
|
+
mode: InterpolationMode = "preview",
|
|
24
36
|
): InterpolationResult {
|
|
25
37
|
const missing = new Set<string>();
|
|
26
38
|
const output = text.replace(TOKEN_PATTERN, (token, path: string) => {
|
|
27
39
|
const value = getPath(data, path);
|
|
28
40
|
if (!isPresent(value)) {
|
|
29
41
|
missing.add(path);
|
|
30
|
-
return token;
|
|
42
|
+
return MISSING_TOKEN[mode](token);
|
|
31
43
|
}
|
|
32
44
|
return formatValue(value);
|
|
33
45
|
});
|
|
@@ -9,7 +9,12 @@ import {
|
|
|
9
9
|
UnregisterHook,
|
|
10
10
|
} from "@antelopejs/interface-dms/hooks";
|
|
11
11
|
import { MODULE_ID } from "../constants";
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
SettingsModel,
|
|
14
|
+
SuppressionModel,
|
|
15
|
+
TemplateModel,
|
|
16
|
+
TemplateVersionModel,
|
|
17
|
+
} from "../db";
|
|
13
18
|
import type { MailingSettingsValues } from "../types";
|
|
14
19
|
|
|
15
20
|
export type ExportedSettings = Omit<MailingSettingsValues, "webhookSecret">;
|
|
@@ -32,7 +37,9 @@ export interface ExportedCollection {
|
|
|
32
37
|
export const TENANT_EXPORT_PAGE_SIZE = 200;
|
|
33
38
|
|
|
34
39
|
const TEMPLATES_ENTRY = "templates.json";
|
|
40
|
+
const TEMPLATE_VERSIONS_ENTRY = "template-versions.json";
|
|
35
41
|
const SETTINGS_ENTRY = "settings.json";
|
|
42
|
+
const SUPPRESSIONS_ENTRY = "suppressions.json";
|
|
36
43
|
|
|
37
44
|
const ARRAY_OPEN = "[";
|
|
38
45
|
const ARRAY_CLOSE = "]";
|
|
@@ -64,7 +71,7 @@ export async function* jsonArrayChunks<T>(
|
|
|
64
71
|
async function exportedSettings(
|
|
65
72
|
tenantId: string,
|
|
66
73
|
): Promise<ExportedSettings | null> {
|
|
67
|
-
const row = await GetModel(SettingsModel, tenantId).
|
|
74
|
+
const row = await GetModel(SettingsModel, tenantId).get(tenantId);
|
|
68
75
|
if (!row) return null;
|
|
69
76
|
return {
|
|
70
77
|
fallbackLocale: row.fallbackLocale,
|
|
@@ -93,12 +100,24 @@ async function loadPage<T extends object>(
|
|
|
93
100
|
|
|
94
101
|
export function exportedCollections(tenantId: string): ExportedCollection[] {
|
|
95
102
|
const templates = GetModel(TemplateModel, tenantId);
|
|
103
|
+
const versions = GetModel(TemplateVersionModel, tenantId);
|
|
104
|
+
const suppressions = GetModel(SuppressionModel, tenantId);
|
|
96
105
|
return [
|
|
97
106
|
{
|
|
98
107
|
entry: TEMPLATES_ENTRY,
|
|
99
108
|
load: (offset, size) =>
|
|
100
109
|
loadPage(templates.table, TemplateModel, offset, size),
|
|
101
110
|
},
|
|
111
|
+
{
|
|
112
|
+
entry: TEMPLATE_VERSIONS_ENTRY,
|
|
113
|
+
load: (offset, size) =>
|
|
114
|
+
loadPage(versions.table, TemplateVersionModel, offset, size),
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
entry: SUPPRESSIONS_ENTRY,
|
|
118
|
+
load: (offset, size) =>
|
|
119
|
+
loadPage(suppressions.table, SuppressionModel, offset, size),
|
|
120
|
+
},
|
|
102
121
|
];
|
|
103
122
|
}
|
|
104
123
|
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { recordEmailEvent as RecordEmailEvent } from "../../services/events";
|
|
2
|
-
export {
|
|
2
|
+
export { sendUnlessSuppressed as SendTemplate } from "../../services/checked-send";
|