@antelopejs/dms-mailing 0.3.3 → 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 +4 -1
- package/frontend-vue/pnpm-lock.yaml +46 -161
- package/frontend-vue/tests/api-error.test.ts +27 -0
- package/frontend-vue/tests/autocomplete.test.ts +84 -0
- package/frontend-vue/tests/block-components.test.ts +1 -1
- package/frontend-vue/tests/block-fields.test.ts +12 -2
- package/frontend-vue/tests/blocks-factory.test.ts +46 -0
- package/frontend-vue/tests/blocks-outline.test.ts +102 -0
- package/frontend-vue/tests/blocks-tree.test.ts +28 -1
- package/frontend-vue/tests/categories-input.test.ts +139 -0
- package/frontend-vue/tests/categories.test.ts +148 -15
- package/frontend-vue/tests/changes.test.ts +136 -0
- package/frontend-vue/tests/column-displays.test.ts +73 -0
- package/frontend-vue/tests/conditions.test.ts +49 -19
- package/frontend-vue/tests/dnd.test.ts +10 -0
- package/frontend-vue/tests/editor-block-list.test.ts +24 -2
- package/frontend-vue/tests/editor-checks.test.ts +178 -0
- package/frontend-vue/tests/editor-route.test.ts +37 -9
- package/frontend-vue/tests/editor-session.test.ts +206 -0
- package/frontend-vue/tests/footer-block.test.ts +88 -0
- package/frontend-vue/tests/funnel-card.test.ts +116 -0
- package/frontend-vue/tests/funnel.test.ts +81 -0
- package/frontend-vue/tests/gallery.test.ts +124 -2
- package/frontend-vue/tests/helpers/nuxt-globals.ts +232 -0
- package/frontend-vue/tests/history.test.ts +48 -0
- package/frontend-vue/tests/i18n-editor.test.ts +38 -0
- package/frontend-vue/tests/i18n.test.ts +55 -0
- package/frontend-vue/tests/lifecycle.test.ts +145 -0
- package/frontend-vue/tests/locales.test.ts +60 -0
- package/frontend-vue/tests/localized-defaults.test.ts +54 -0
- package/frontend-vue/tests/new-template-modal.test.ts +210 -0
- package/frontend-vue/tests/overview-gate.test.ts +146 -0
- package/frontend-vue/tests/overview-setup.test.ts +78 -0
- package/frontend-vue/tests/paths.test.ts +23 -0
- package/frontend-vue/tests/period.test.ts +59 -10
- package/frontend-vue/tests/problems.test.ts +153 -0
- package/frontend-vue/tests/recipients.test.ts +47 -0
- package/frontend-vue/tests/rendering-title.test.ts +24 -0
- package/frontend-vue/tests/replay-toast.test.ts +67 -0
- package/frontend-vue/tests/retention-input.test.ts +129 -0
- package/frontend-vue/tests/send-actions.test.ts +128 -0
- package/frontend-vue/tests/send-modals.test.ts +415 -0
- package/frontend-vue/tests/send-snippet.test.ts +51 -0
- package/frontend-vue/tests/send-stats.test.ts +12 -0
- package/frontend-vue/tests/send-values.test.ts +119 -0
- package/frontend-vue/tests/shortcuts.test.ts +75 -0
- package/frontend-vue/tests/slug.test.ts +88 -0
- package/frontend-vue/tests/suppression.test.ts +159 -0
- package/frontend-vue/tests/system-variables.test.ts +133 -0
- package/frontend-vue/tests/template-attention.test.ts +123 -0
- package/frontend-vue/tests/template-drawer.test.ts +449 -0
- package/frontend-vue/tests/template-editor.test.ts +218 -9
- package/frontend-vue/tests/template-gallery-display.test.ts +32 -0
- package/frontend-vue/tests/template-gallery.test.ts +370 -0
- package/frontend-vue/tests/template-overlays.test.ts +125 -0
- package/frontend-vue/tests/timeline.test.ts +175 -20
- package/frontend-vue/tests/tokens.test.ts +79 -1
- package/frontend-vue/tests/variable-path.test.ts +22 -0
- package/frontend-vue/tests/variable-types.test.ts +20 -0
- package/frontend-vue/tests/variables.test.ts +90 -2
- package/frontend-vue/tests/version-history.test.ts +41 -0
- package/frontend-vue/tests/webhook-status.test.ts +108 -0
- package/package.json +11 -9
- package/src/automation/email-event-trigger.ts +3 -3
- package/src/automation/send-template-action.ts +7 -5
- package/src/config.ts +3 -1
- package/src/constants.ts +18 -2
- package/src/crons/attention.ts +21 -0
- package/src/crons/index.ts +2 -0
- package/src/crons/jobs.ts +35 -0
- package/src/crons/retention.ts +7 -22
- package/src/data/displays.ts +18 -0
- package/src/data/index.ts +1 -0
- package/src/data/locale-fallback.ts +26 -0
- package/src/data/read-only-routes.ts +14 -0
- package/src/data/sends-table.ts +204 -19
- package/src/data/suppressions-table.ts +127 -0
- package/src/data/templates-table.ts +295 -63
- package/src/data-types/categories-type.ts +4 -5
- package/src/data-types/category-type.ts +1 -1
- package/src/db/models/index.ts +3 -0
- package/src/db/models/sends.model.ts +102 -1
- package/src/db/models/settings.model.ts +2 -12
- package/src/db/models/suppressions.model.ts +53 -0
- package/src/db/models/template-versions.model.ts +23 -0
- package/src/db/models/templates.model.ts +17 -0
- package/src/db/models/webhook-status.model.ts +25 -0
- package/src/db/tables/index.ts +3 -0
- package/src/db/tables/send-events.table.ts +6 -2
- package/src/db/tables/sends.table.ts +37 -2
- package/src/db/tables/suppressions.table.ts +46 -0
- package/src/db/tables/template-versions.table.ts +44 -0
- package/src/db/tables/templates.table.ts +67 -5
- package/src/db/tables/webhook-status.table.ts +44 -0
- package/src/engine/changes.ts +198 -0
- package/src/engine/conditions.ts +1 -7
- package/src/engine/coverage.ts +7 -2
- package/src/engine/fallback-html.ts +80 -42
- package/src/engine/index.ts +3 -0
- package/src/engine/plain-text.ts +86 -0
- package/src/engine/resolve.ts +49 -15
- package/src/engine/system-variables.ts +124 -0
- package/src/engine/tokens.ts +13 -1
- package/src/hooks/tenant-export.ts +21 -2
- package/src/implementations/dms-mailing/index.ts +1 -1
- package/src/index.ts +29 -13
- package/src/pages/editor.ts +13 -2
- package/src/pages/index.ts +6 -3
- package/src/pages/module.ts +31 -2
- package/src/pages/overview.ts +187 -128
- package/src/pages/sends.ts +271 -112
- package/src/pages/settings/form.ts +144 -54
- package/src/pages/settings/page.ts +9 -17
- package/src/pages/suppressions.ts +157 -0
- package/src/pages/templates.ts +139 -42
- package/src/permissions.ts +6 -4
- package/src/quick-actions.ts +12 -5
- package/src/routes/events.ts +110 -33
- package/src/routes/index.ts +2 -0
- package/src/routes/metrics.ts +348 -98
- package/src/routes/period.ts +23 -6
- package/src/routes/sends.ts +413 -41
- package/src/routes/settings.ts +291 -26
- package/src/routes/suppressions.ts +87 -0
- package/src/routes/template-versions.ts +116 -0
- package/src/routes/templates.ts +334 -48
- package/src/services/attention.ts +283 -30
- package/src/services/catalog.ts +28 -0
- package/src/services/categories.ts +28 -27
- package/src/services/checked-send.ts +123 -0
- package/src/services/events.ts +174 -20
- package/src/services/metrics.ts +137 -28
- package/src/services/notify.ts +126 -44
- package/src/services/provider-banner.ts +165 -0
- package/src/services/render.ts +1 -0
- package/src/services/send-stat-items.ts +160 -0
- package/src/services/send-stats.ts +122 -0
- package/src/services/send.ts +193 -31
- package/src/services/settings.ts +52 -8
- package/src/services/starters.ts +383 -0
- package/src/services/suppressions.ts +116 -0
- package/src/services/template-attention.ts +82 -0
- package/src/services/template-stats.ts +97 -0
- package/src/services/templates.ts +400 -37
- package/src/services/webhook-status.ts +202 -0
- package/src/types/blocks.ts +6 -0
- package/src/types/index.ts +2 -0
- package/src/types/sends.ts +64 -2
- package/src/types/suppressions.ts +27 -0
- package/src/types/webhook.ts +29 -0
- package/src/validation/settings.schema.ts +173 -17
- package/src/validation/templates.schema.ts +53 -1
- package/dist/antelope-modules.d.ts +0 -32
- package/dist/antelope-modules.js +0 -36
- package/dist/antelope-modules.js.map +0 -1
- package/dist/test/antelope.test.d.ts +0 -2
- package/dist/test/antelope.test.js +0 -180
- package/dist/test/antelope.test.js.map +0 -1
- package/dist/test/helpers/http.d.ts +0 -4
- package/dist/test/helpers/http.js +0 -28
- package/dist/test/helpers/http.js.map +0 -1
- package/dist/test/helpers/owner.d.ts +0 -5
- package/dist/test/helpers/owner.js +0 -30
- package/dist/test/helpers/owner.js.map +0 -1
- package/dist/test/integration/events.test.d.ts +0 -1
- package/dist/test/integration/events.test.js +0 -143
- package/dist/test/integration/events.test.js.map +0 -1
- package/dist/test/integration/retention.test.d.ts +0 -1
- package/dist/test/integration/retention.test.js +0 -291
- package/dist/test/integration/retention.test.js.map +0 -1
- package/dist/test/integration/sends.test.d.ts +0 -1
- package/dist/test/integration/sends.test.js +0 -210
- package/dist/test/integration/sends.test.js.map +0 -1
- package/dist/test/integration/settings.test.d.ts +0 -1
- package/dist/test/integration/settings.test.js +0 -137
- package/dist/test/integration/settings.test.js.map +0 -1
- package/dist/test/integration/setup.test.d.ts +0 -1
- package/dist/test/integration/setup.test.js +0 -32
- package/dist/test/integration/setup.test.js.map +0 -1
- package/dist/test/integration/templates.test.d.ts +0 -1
- package/dist/test/integration/templates.test.js +0 -233
- package/dist/test/integration/templates.test.js.map +0 -1
- package/dist/test/integration/tenant-export.test.d.ts +0 -1
- package/dist/test/integration/tenant-export.test.js +0 -85
- package/dist/test/integration/tenant-export.test.js.map +0 -1
- package/dist/test/unit/attention.test.d.ts +0 -1
- package/dist/test/unit/attention.test.js +0 -109
- package/dist/test/unit/attention.test.js.map +0 -1
- package/dist/test/unit/automation.test.d.ts +0 -1
- package/dist/test/unit/automation.test.js +0 -104
- package/dist/test/unit/automation.test.js.map +0 -1
- package/dist/test/unit/categories.test.d.ts +0 -1
- package/dist/test/unit/categories.test.js +0 -38
- package/dist/test/unit/categories.test.js.map +0 -1
- package/dist/test/unit/engine/conditions.test.d.ts +0 -1
- package/dist/test/unit/engine/conditions.test.js +0 -23
- package/dist/test/unit/engine/conditions.test.js.map +0 -1
- package/dist/test/unit/engine/coverage.test.d.ts +0 -1
- package/dist/test/unit/engine/coverage.test.js +0 -106
- package/dist/test/unit/engine/coverage.test.js.map +0 -1
- package/dist/test/unit/engine/fallback-html.test.d.ts +0 -1
- package/dist/test/unit/engine/fallback-html.test.js +0 -78
- package/dist/test/unit/engine/fallback-html.test.js.map +0 -1
- package/dist/test/unit/engine/paths.test.d.ts +0 -1
- package/dist/test/unit/engine/paths.test.js +0 -33
- package/dist/test/unit/engine/paths.test.js.map +0 -1
- package/dist/test/unit/engine/resolve.test.d.ts +0 -1
- package/dist/test/unit/engine/resolve.test.js +0 -157
- package/dist/test/unit/engine/resolve.test.js.map +0 -1
- package/dist/test/unit/engine/tokens.test.d.ts +0 -1
- package/dist/test/unit/engine/tokens.test.js +0 -26
- package/dist/test/unit/engine/tokens.test.js.map +0 -1
- package/dist/test/unit/events.test.d.ts +0 -1
- package/dist/test/unit/events.test.js +0 -13
- package/dist/test/unit/events.test.js.map +0 -1
- package/dist/test/unit/metrics.test.d.ts +0 -1
- package/dist/test/unit/metrics.test.js +0 -108
- package/dist/test/unit/metrics.test.js.map +0 -1
- package/dist/test/unit/notify.test.d.ts +0 -1
- package/dist/test/unit/notify.test.js +0 -49
- package/dist/test/unit/notify.test.js.map +0 -1
- package/dist/test/unit/period.test.d.ts +0 -1
- package/dist/test/unit/period.test.js +0 -61
- package/dist/test/unit/period.test.js.map +0 -1
- package/dist/test/unit/permissions.test.d.ts +0 -1
- package/dist/test/unit/permissions.test.js +0 -27
- package/dist/test/unit/permissions.test.js.map +0 -1
- package/dist/test/unit/provider.test.d.ts +0 -1
- package/dist/test/unit/provider.test.js +0 -47
- package/dist/test/unit/provider.test.js.map +0 -1
- package/dist/test/unit/realtime.test.d.ts +0 -1
- package/dist/test/unit/realtime.test.js +0 -13
- package/dist/test/unit/realtime.test.js.map +0 -1
- package/dist/test/unit/render.test.d.ts +0 -1
- package/dist/test/unit/render.test.js +0 -23
- package/dist/test/unit/render.test.js.map +0 -1
- package/dist/test/unit/retention.test.d.ts +0 -1
- package/dist/test/unit/retention.test.js +0 -66
- package/dist/test/unit/retention.test.js.map +0 -1
- package/dist/test/unit/send-batch.test.d.ts +0 -1
- package/dist/test/unit/send-batch.test.js +0 -46
- package/dist/test/unit/send-batch.test.js.map +0 -1
- package/dist/test/unit/send.test.d.ts +0 -1
- package/dist/test/unit/send.test.js +0 -120
- package/dist/test/unit/send.test.js.map +0 -1
- package/dist/test/unit/smoke.test.d.ts +0 -1
- package/dist/test/unit/smoke.test.js +0 -10
- package/dist/test/unit/smoke.test.js.map +0 -1
- package/dist/test/unit/status.test.d.ts +0 -1
- package/dist/test/unit/status.test.js +0 -16
- package/dist/test/unit/status.test.js.map +0 -1
- package/dist/test/unit/templates.test.d.ts +0 -1
- package/dist/test/unit/templates.test.js +0 -48
- package/dist/test/unit/templates.test.js.map +0 -1
- package/dist/test/unit/tenant-export.test.d.ts +0 -1
- package/dist/test/unit/tenant-export.test.js +0 -59
- package/dist/test/unit/tenant-export.test.js.map +0 -1
- package/dist/test/unit/webhook-secret.test.d.ts +0 -1
- package/dist/test/unit/webhook-secret.test.js +0 -43
- package/dist/test/unit/webhook-secret.test.js.map +0 -1
- package/frontend-vue/app/components/AttentionCard.vue +0 -78
- package/frontend-vue/app/components/HtmlPreviewModal.vue +0 -20
- package/frontend-vue/app/components/ProviderChip.vue +0 -39
- package/frontend-vue/app/components/editor/EditorBlockSettings.vue +0 -186
- package/frontend-vue/app/components/editor/EditorDataPanel.vue +0 -130
- package/frontend-vue/app/components/editor/EditorDraftBar.vue +0 -35
- package/frontend-vue/app/components/editor/EditorRail.vue +0 -58
- package/frontend-vue/app/components/editor/EditorSubjectSettings.vue +0 -37
- package/frontend-vue/app/components/editor/EditorTemplateSettings.vue +0 -87
- package/frontend-vue/app/components/editor/EditorTestSendModal.vue +0 -66
- package/frontend-vue/app/components/editor/EditorVariables.vue +0 -163
- package/frontend-vue/app/composables/useMailingPeriodData.ts +0 -39
- package/src/antelope-modules.ts +0 -48
- package/src/test/antelope.test.ts +0 -190
- package/src/test/helpers/http.ts +0 -23
- package/src/test/helpers/owner.ts +0 -37
- package/src/test/integration/events.test.ts +0 -159
- package/src/test/integration/retention.test.ts +0 -363
- package/src/test/integration/sends.test.ts +0 -233
- package/src/test/integration/settings.test.ts +0 -128
- package/src/test/integration/setup.test.ts +0 -35
- package/src/test/integration/templates.test.ts +0 -272
- package/src/test/integration/tenant-export.test.ts +0 -125
- package/src/test/unit/attention.test.ts +0 -129
- package/src/test/unit/automation.test.ts +0 -130
- package/src/test/unit/categories.test.ts +0 -51
- package/src/test/unit/engine/conditions.test.ts +0 -35
- package/src/test/unit/engine/coverage.test.ts +0 -118
- package/src/test/unit/engine/fallback-html.test.ts +0 -86
- package/src/test/unit/engine/paths.test.ts +0 -39
- package/src/test/unit/engine/resolve.test.ts +0 -176
- package/src/test/unit/engine/tokens.test.ts +0 -26
- package/src/test/unit/events.test.ts +0 -19
- package/src/test/unit/metrics.test.ts +0 -170
- package/src/test/unit/notify.test.ts +0 -71
- package/src/test/unit/period.test.ts +0 -91
- package/src/test/unit/permissions.test.ts +0 -30
- package/src/test/unit/provider.test.ts +0 -57
- package/src/test/unit/realtime.test.ts +0 -11
- package/src/test/unit/render.test.ts +0 -23
- package/src/test/unit/retention.test.ts +0 -91
- package/src/test/unit/send-batch.test.ts +0 -72
- package/src/test/unit/send.test.ts +0 -161
- package/src/test/unit/smoke.test.ts +0 -8
- package/src/test/unit/status.test.ts +0 -14
- package/src/test/unit/templates.test.ts +0 -52
- package/src/test/unit/tenant-export.test.ts +0 -88
- package/src/test/unit/webhook-secret.test.ts +0 -51
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import type { VariableDefinition, VariableType } from '../types/mailing'
|
|
2
|
+
import { deepClone } from './clone'
|
|
3
|
+
import { isPresent, readPath } from './paths'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* How a value of a real send is entered: typed, ticked, or a list or an object
|
|
7
|
+
* taken as is from the test data (neither has a one-line form).
|
|
8
|
+
*/
|
|
9
|
+
export type SendValueKind = 'text' | 'boolean' | 'list' | 'object'
|
|
10
|
+
|
|
11
|
+
/** One variable the real-send dialog asks a value for. */
|
|
12
|
+
export interface SendField {
|
|
13
|
+
path: string
|
|
14
|
+
required: boolean
|
|
15
|
+
kind: SendValueKind
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** The dialog's values, by variable path. */
|
|
19
|
+
export type SendValues = Record<string, unknown>
|
|
20
|
+
|
|
21
|
+
const PATH_SEPARATOR = '.'
|
|
22
|
+
const ARRAY_SUFFIX = '[]'
|
|
23
|
+
const RESERVED_SEGMENTS = ['__proto__', 'constructor', 'prototype']
|
|
24
|
+
|
|
25
|
+
const KIND_BY_TYPE: Partial<Record<VariableType, SendValueKind>> = {
|
|
26
|
+
boolean: 'boolean',
|
|
27
|
+
array: 'list',
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function kindOfValue(value: unknown): SendValueKind {
|
|
31
|
+
if (typeof value === 'boolean') return 'boolean'
|
|
32
|
+
if (Array.isArray(value)) return 'list'
|
|
33
|
+
return value !== null && typeof value === 'object' ? 'object' : 'text'
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function normalize(path: string): string {
|
|
37
|
+
const trimmed = path.trim()
|
|
38
|
+
return trimmed.endsWith(ARRAY_SUFFIX)
|
|
39
|
+
? trimmed.slice(0, -ARRAY_SUFFIX.length)
|
|
40
|
+
: trimmed
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Whether `path` holds `other` (`order` holds `order.number`). */
|
|
44
|
+
function isAncestor(path: string, other: string): boolean {
|
|
45
|
+
return other.startsWith(`${path}${PATH_SEPARATOR}`)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The variables a real send needs values for: the declared ones first, then
|
|
50
|
+
* the ones the content uses without declaring them, never required. An
|
|
51
|
+
* undeclared path whose children are asked for (`{{order}}` next to
|
|
52
|
+
* `{{order.number}}`) gets no field of its own: its children build it.
|
|
53
|
+
*/
|
|
54
|
+
export function sendFields(
|
|
55
|
+
declared: VariableDefinition[],
|
|
56
|
+
detected: string[],
|
|
57
|
+
testData: Record<string, unknown>,
|
|
58
|
+
): SendField[] {
|
|
59
|
+
const fields = declared.map((variable) => ({
|
|
60
|
+
path: normalize(variable.path),
|
|
61
|
+
required: variable.required,
|
|
62
|
+
kind:
|
|
63
|
+
KIND_BY_TYPE[variable.type] ??
|
|
64
|
+
kindOfValue(readPath(testData, variable.path)),
|
|
65
|
+
}))
|
|
66
|
+
const known = new Set(fields.map((field) => field.path))
|
|
67
|
+
const paths = [...new Set(detected.map(normalize))].filter(
|
|
68
|
+
(path) => path && !known.has(path),
|
|
69
|
+
)
|
|
70
|
+
const every = [...known, ...paths]
|
|
71
|
+
const undeclared = paths
|
|
72
|
+
.filter((path) => !every.some((other) => isAncestor(path, other)))
|
|
73
|
+
.map((path) => ({
|
|
74
|
+
path,
|
|
75
|
+
required: false,
|
|
76
|
+
kind: kindOfValue(readPath(testData, path)),
|
|
77
|
+
}))
|
|
78
|
+
return [...fields, ...undeclared]
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const INITIAL_VALUES: Record<SendValueKind, (value: unknown) => unknown> = {
|
|
82
|
+
text: (value) => (isPresent(value) ? String(value) : ''),
|
|
83
|
+
boolean: (value) => Boolean(value),
|
|
84
|
+
list: (value) => (isPresent(value) ? value : undefined),
|
|
85
|
+
object: (value) => (isPresent(value) ? value : undefined),
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** The values the dialog opens with: the template's test data. */
|
|
89
|
+
export function initialSendValues(
|
|
90
|
+
fields: SendField[],
|
|
91
|
+
testData: Record<string, unknown>,
|
|
92
|
+
): SendValues {
|
|
93
|
+
return Object.fromEntries(
|
|
94
|
+
fields.map((field) => [
|
|
95
|
+
field.path,
|
|
96
|
+
INITIAL_VALUES[field.kind](readPath(testData, field.path)),
|
|
97
|
+
]),
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const HAS_VALUE: Record<SendValueKind, (value: unknown) => boolean> = {
|
|
102
|
+
text: (value) => typeof value === 'string' && value.trim() !== '',
|
|
103
|
+
boolean: () => true,
|
|
104
|
+
list: isPresent,
|
|
105
|
+
object: isPresent,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** The required variables still without a value: the send waits for them. */
|
|
109
|
+
export function missingRequiredPaths(
|
|
110
|
+
fields: SendField[],
|
|
111
|
+
values: SendValues,
|
|
112
|
+
): string[] {
|
|
113
|
+
return fields
|
|
114
|
+
.filter((field) => field.required)
|
|
115
|
+
.filter((field) => !HAS_VALUE[field.kind](values[field.path]))
|
|
116
|
+
.map((field) => field.path)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function setPath(
|
|
120
|
+
target: Record<string, unknown>,
|
|
121
|
+
path: string,
|
|
122
|
+
value: unknown,
|
|
123
|
+
): void {
|
|
124
|
+
const segments = path.split(PATH_SEPARATOR)
|
|
125
|
+
if (segments.some((segment) => RESERVED_SEGMENTS.includes(segment))) return
|
|
126
|
+
const last = segments.pop() as string
|
|
127
|
+
const parent = segments.reduce<Record<string, unknown>>((node, segment) => {
|
|
128
|
+
const child = node[segment]
|
|
129
|
+
if (child === null || typeof child !== 'object') node[segment] = {}
|
|
130
|
+
return node[segment] as Record<string, unknown>
|
|
131
|
+
}, target)
|
|
132
|
+
parent[last] = value
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function depth(field: SendField): number {
|
|
136
|
+
return field.path.split(PATH_SEPARATOR).length
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The `data` a real send carries: every value given, nested by its path. The
|
|
141
|
+
* shallow paths go first, copied, so a value typed for `order.number` always
|
|
142
|
+
* wins over the `order` object taken from the test data.
|
|
143
|
+
*/
|
|
144
|
+
export function buildSendData(
|
|
145
|
+
fields: SendField[],
|
|
146
|
+
values: SendValues,
|
|
147
|
+
): Record<string, unknown> {
|
|
148
|
+
const data: Record<string, unknown> = {}
|
|
149
|
+
fields
|
|
150
|
+
.filter((field) => HAS_VALUE[field.kind](values[field.path]))
|
|
151
|
+
.sort((left, right) => depth(left) - depth(right))
|
|
152
|
+
.forEach((field) =>
|
|
153
|
+
setPath(data, field.path, deepClone(values[field.path])),
|
|
154
|
+
)
|
|
155
|
+
return data
|
|
156
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
export type EditorShortcut =
|
|
2
|
+
| 'save'
|
|
3
|
+
| 'publish'
|
|
4
|
+
| 'test'
|
|
5
|
+
| 'search'
|
|
6
|
+
| 'delete'
|
|
7
|
+
| 'duplicate'
|
|
8
|
+
| 'escape'
|
|
9
|
+
| 'undo'
|
|
10
|
+
| 'redo'
|
|
11
|
+
|
|
12
|
+
/** The parts of a `KeyboardEvent` a shortcut is matched on. */
|
|
13
|
+
export interface KeyStroke {
|
|
14
|
+
key: string
|
|
15
|
+
metaKey: boolean
|
|
16
|
+
ctrlKey: boolean
|
|
17
|
+
shiftKey: boolean
|
|
18
|
+
altKey: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface ShortcutRule {
|
|
22
|
+
shortcut: EditorShortcut
|
|
23
|
+
keys: string[]
|
|
24
|
+
hasModifier: boolean
|
|
25
|
+
hasShift?: boolean
|
|
26
|
+
isAllowedWhileTyping: boolean
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const rule = (
|
|
30
|
+
shortcut: EditorShortcut,
|
|
31
|
+
keys: string[],
|
|
32
|
+
hasModifier: boolean,
|
|
33
|
+
isAllowedWhileTyping: boolean,
|
|
34
|
+
hasShift?: boolean,
|
|
35
|
+
): ShortcutRule => ({
|
|
36
|
+
shortcut,
|
|
37
|
+
keys,
|
|
38
|
+
hasModifier,
|
|
39
|
+
isAllowedWhileTyping,
|
|
40
|
+
hasShift,
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const RULES: ShortcutRule[] = [
|
|
44
|
+
rule('save', ['s'], true, true),
|
|
45
|
+
rule('publish', ['enter'], true, true),
|
|
46
|
+
rule('duplicate', ['d'], true, false),
|
|
47
|
+
rule('redo', ['z'], true, false, true),
|
|
48
|
+
rule('redo', ['y'], true, false),
|
|
49
|
+
rule('undo', ['z'], true, false, false),
|
|
50
|
+
rule('test', ['t'], false, false),
|
|
51
|
+
rule('search', ['/'], false, false),
|
|
52
|
+
rule('delete', ['backspace', 'delete'], false, false),
|
|
53
|
+
rule('escape', ['escape'], false, false),
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
const NON_TEXT_INPUT_TYPES = [
|
|
57
|
+
'button',
|
|
58
|
+
'checkbox',
|
|
59
|
+
'radio',
|
|
60
|
+
'range',
|
|
61
|
+
'reset',
|
|
62
|
+
'submit',
|
|
63
|
+
'color',
|
|
64
|
+
'file',
|
|
65
|
+
]
|
|
66
|
+
const TEXT_TAGS = ['TEXTAREA', 'SELECT']
|
|
67
|
+
const INPUT_TAG = 'INPUT'
|
|
68
|
+
const EDITABLE_SELECTOR = '[contenteditable=""], [contenteditable="true"]'
|
|
69
|
+
|
|
70
|
+
interface TargetLike {
|
|
71
|
+
tagName?: string
|
|
72
|
+
type?: string
|
|
73
|
+
isContentEditable?: boolean
|
|
74
|
+
closest?: (selector: string) => unknown
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Whether the keystroke lands in something the user types in: single-key
|
|
79
|
+
* shortcuts (T, /, ⌫) must leave those alone.
|
|
80
|
+
*/
|
|
81
|
+
export function isTypingTarget(target: EventTarget | null): boolean {
|
|
82
|
+
const element = target as TargetLike | null
|
|
83
|
+
if (!element?.tagName) return false
|
|
84
|
+
if (TEXT_TAGS.includes(element.tagName)) return true
|
|
85
|
+
if (element.tagName === INPUT_TAG)
|
|
86
|
+
return !NON_TEXT_INPUT_TYPES.includes(element.type ?? '')
|
|
87
|
+
if (element.isContentEditable) return true
|
|
88
|
+
return Boolean(element.closest?.(EDITABLE_SELECTOR))
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function matches(stroke: KeyStroke, candidate: ShortcutRule): boolean {
|
|
92
|
+
const hasModifier = stroke.metaKey || stroke.ctrlKey
|
|
93
|
+
if (stroke.altKey || hasModifier !== candidate.hasModifier) return false
|
|
94
|
+
if (
|
|
95
|
+
candidate.hasShift !== undefined &&
|
|
96
|
+
candidate.hasShift !== stroke.shiftKey
|
|
97
|
+
)
|
|
98
|
+
return false
|
|
99
|
+
return candidate.keys.includes(stroke.key.toLowerCase())
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** The editor shortcut a keystroke triggers, or `null`. */
|
|
103
|
+
export function matchShortcut(
|
|
104
|
+
stroke: KeyStroke,
|
|
105
|
+
isTyping: boolean,
|
|
106
|
+
): EditorShortcut | null {
|
|
107
|
+
const found = RULES.find(
|
|
108
|
+
(candidate) =>
|
|
109
|
+
matches(stroke, candidate) &&
|
|
110
|
+
(!isTyping || candidate.isAllowedWhileTyping),
|
|
111
|
+
)
|
|
112
|
+
return found?.shortcut ?? null
|
|
113
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/** Mirrors the backend `SLUG_PATTERN`: what `SendTemplate(slug)` accepts. */
|
|
2
|
+
export const SLUG_PATTERN = /^[a-z0-9][a-z0-9-]*$/
|
|
3
|
+
|
|
4
|
+
export const MAX_SLUG_LENGTH = 64
|
|
5
|
+
|
|
6
|
+
export const MAX_NAME_LENGTH = 120
|
|
7
|
+
|
|
8
|
+
const SLUG_SEPARATOR = '-'
|
|
9
|
+
|
|
10
|
+
const FIRST_FREE_SUFFIX = 2
|
|
11
|
+
|
|
12
|
+
export type SlugIssue = 'required' | 'format' | 'too_long' | 'taken'
|
|
13
|
+
|
|
14
|
+
export type NameIssue = 'required' | 'too_long'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The slug a name suggests: ASCII, lowercase, words joined by dashes, cut to
|
|
18
|
+
* the maximum length without a trailing dash.
|
|
19
|
+
*/
|
|
20
|
+
export function slugify(name: string): string {
|
|
21
|
+
return name
|
|
22
|
+
.normalize('NFD')
|
|
23
|
+
.replace(/\p{Diacritic}/gu, '')
|
|
24
|
+
.toLowerCase()
|
|
25
|
+
.replace(/[^a-z0-9]+/g, SLUG_SEPARATOR)
|
|
26
|
+
.replace(/^-+|-+$/g, '')
|
|
27
|
+
.slice(0, MAX_SLUG_LENGTH)
|
|
28
|
+
.replace(/-+$/, '')
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* What is wrong with a slug, or `null` when it can be created: present, in
|
|
33
|
+
* the backend format, short enough and not used by another template.
|
|
34
|
+
*/
|
|
35
|
+
export function validateSlug(
|
|
36
|
+
slug: string,
|
|
37
|
+
takenSlugs: ReadonlySet<string>,
|
|
38
|
+
): SlugIssue | null {
|
|
39
|
+
if (!slug) return 'required'
|
|
40
|
+
if (slug.length > MAX_SLUG_LENGTH) return 'too_long'
|
|
41
|
+
if (!SLUG_PATTERN.test(slug)) return 'format'
|
|
42
|
+
return takenSlugs.has(slug) ? 'taken' : null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** What is wrong with a template name, or `null` when it is fine. */
|
|
46
|
+
export function validateName(name: string): NameIssue | null {
|
|
47
|
+
const trimmed = name.trim()
|
|
48
|
+
if (!trimmed) return 'required'
|
|
49
|
+
return trimmed.length > MAX_NAME_LENGTH ? 'too_long' : null
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** `base` with a `-N` suffix, the base cut so the whole fits the slug limit. */
|
|
53
|
+
function suffixedSlug(base: string, suffix: number): string {
|
|
54
|
+
const ending = `${SLUG_SEPARATOR}${suffix}`
|
|
55
|
+
const kept = base.slice(0, MAX_SLUG_LENGTH - ending.length).replace(/-+$/, '')
|
|
56
|
+
return `${kept}${ending}`
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The first free slug from `base`: itself, else `base-2`, `base-3`… cut to
|
|
61
|
+
* the slug limit, so a duplicate of "welcome (copy)" never starts out refused.
|
|
62
|
+
*/
|
|
63
|
+
export function freeSlug(
|
|
64
|
+
base: string,
|
|
65
|
+
takenSlugs: ReadonlySet<string>,
|
|
66
|
+
): string {
|
|
67
|
+
if (!takenSlugs.has(base)) return base
|
|
68
|
+
let suffix = FIRST_FREE_SUFFIX
|
|
69
|
+
while (takenSlugs.has(suffixedSlug(base, suffix))) suffix += 1
|
|
70
|
+
return suffixedSlug(base, suffix)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* A name wrapped by `wrap` ("{name} (copy)"), the inner name cut so the whole
|
|
75
|
+
* stays within the name limit.
|
|
76
|
+
*/
|
|
77
|
+
export function fitName(name: string, wrap: (inner: string) => string): string {
|
|
78
|
+
const wrapped = wrap(name)
|
|
79
|
+
if (wrapped.length <= MAX_NAME_LENGTH) return wrapped
|
|
80
|
+
const room = Math.max(0, MAX_NAME_LENGTH - wrap('').length)
|
|
81
|
+
return wrap(name.slice(0, room).trimEnd())
|
|
82
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { StarterSummary } from '../types/mailing'
|
|
2
|
+
import type { OptionalTranslate } from './categories'
|
|
3
|
+
|
|
4
|
+
const STARTER_NAME_KEY_PREFIX = 'dms_mailing.starters.names'
|
|
5
|
+
|
|
6
|
+
/** The i18n key naming a shipped starter ("Order confirmation"). */
|
|
7
|
+
export function starterNameKey(id: string): string {
|
|
8
|
+
return `${STARTER_NAME_KEY_PREFIX}.${id}`
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The starters named in the interface's language; one the catalog does not
|
|
13
|
+
* know keeps the name the backend gives it.
|
|
14
|
+
*/
|
|
15
|
+
export function localizeStarters(
|
|
16
|
+
starters: StarterSummary[],
|
|
17
|
+
translate: OptionalTranslate,
|
|
18
|
+
): StarterSummary[] {
|
|
19
|
+
return starters.map((starter) => ({
|
|
20
|
+
...starter,
|
|
21
|
+
name: translate(starterNameKey(starter.id)) ?? starter.name,
|
|
22
|
+
}))
|
|
23
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { SuppressionReason, SuppressionSummary } from '../types/mailing'
|
|
2
|
+
import { SUPPRESSIONS_PATH } from './pages'
|
|
3
|
+
|
|
4
|
+
/** The status of a send nothing was sent for: its address is suppressed. */
|
|
5
|
+
export const SUPPRESSED_STATUS = 'suppressed'
|
|
6
|
+
|
|
7
|
+
const EMAIL_QUERY_KEY = 'email'
|
|
8
|
+
const KEY_PREFIX = 'dms_mailing.suppressions.'
|
|
9
|
+
|
|
10
|
+
export type { SuppressionReason, SuppressionSummary }
|
|
11
|
+
|
|
12
|
+
/** A banner of the send drawer about the recipient's suppression. */
|
|
13
|
+
export interface SuppressionNotice {
|
|
14
|
+
tone: 'warning' | 'info' | 'success'
|
|
15
|
+
icon: string
|
|
16
|
+
/** i18n keys, interpolated with `params`. */
|
|
17
|
+
titleKey: string
|
|
18
|
+
descriptionKey: string
|
|
19
|
+
params: Record<string, string>
|
|
20
|
+
/** Whether the banner links to the address on the suppression list. */
|
|
21
|
+
hasListLink: boolean
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** What the drawer reads of a send to tell its suppression. */
|
|
25
|
+
export interface SuppressionSend {
|
|
26
|
+
status: string
|
|
27
|
+
recipientEmail: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** How the drawer words a suppression's date and reason. */
|
|
31
|
+
export interface SuppressionWording {
|
|
32
|
+
date: (iso: string) => string
|
|
33
|
+
reason: (reason: SuppressionReason) => string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface SuppressionRoute {
|
|
37
|
+
path: string
|
|
38
|
+
query: Record<string, string>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function isSuppressedStatus(status: string | undefined): boolean {
|
|
42
|
+
return status === SUPPRESSED_STATUS
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The i18n key naming a suppression reason ("Hard bounce"). */
|
|
46
|
+
export function reasonKey(reason: SuppressionReason): string {
|
|
47
|
+
return `${KEY_PREFIX}reasons.${reason}`
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The prefix of the error a suppressed send keeps, one key per reason. */
|
|
51
|
+
const SUPPRESSED_ERROR_PREFIX = '$dms_mailing.errors.suppressed.'
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The reason a suppressed send's error names ("manual" out of
|
|
55
|
+
* `$dms_mailing.errors.suppressed.manual`); `null` for any other error.
|
|
56
|
+
*/
|
|
57
|
+
export function suppressedReasonOf(
|
|
58
|
+
error: string | undefined,
|
|
59
|
+
): SuppressionReason | null {
|
|
60
|
+
if (!error?.startsWith(SUPPRESSED_ERROR_PREFIX)) return null
|
|
61
|
+
return error.slice(SUPPRESSED_ERROR_PREFIX.length) as SuppressionReason
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The suppression list, narrowed to one address. The list filters on the
|
|
66
|
+
* stored address exactly, and addresses are stored trimmed and lowercased.
|
|
67
|
+
*/
|
|
68
|
+
export function suppressionRoute(email: string): SuppressionRoute {
|
|
69
|
+
return {
|
|
70
|
+
path: SUPPRESSIONS_PATH,
|
|
71
|
+
query: { [EMAIL_QUERY_KEY]: email.trim().toLowerCase() },
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const notice = (
|
|
76
|
+
key: string,
|
|
77
|
+
tone: SuppressionNotice['tone'],
|
|
78
|
+
hasListLink: boolean,
|
|
79
|
+
params: Record<string, string>,
|
|
80
|
+
): SuppressionNotice => ({
|
|
81
|
+
tone,
|
|
82
|
+
icon: 'i-ph-prohibit',
|
|
83
|
+
titleKey: `${KEY_PREFIX}notice.${key}.title`,
|
|
84
|
+
descriptionKey: `${KEY_PREFIX}notice.${key}.description`,
|
|
85
|
+
params,
|
|
86
|
+
hasListLink,
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* What the drawer says about the recipient's suppression: a send that was
|
|
91
|
+
* not sent (and whether the address is still suppressed), or an address that
|
|
92
|
+
* receives nothing any more since this send. `null` when there is nothing to
|
|
93
|
+
* say.
|
|
94
|
+
*/
|
|
95
|
+
export function suppressionNotice(
|
|
96
|
+
send: SuppressionSend,
|
|
97
|
+
suppression: SuppressionSummary | null | undefined,
|
|
98
|
+
wording: SuppressionWording,
|
|
99
|
+
): SuppressionNotice | null {
|
|
100
|
+
const params = suppression
|
|
101
|
+
? {
|
|
102
|
+
date: wording.date(suppression.createdAt),
|
|
103
|
+
reason: wording.reason(suppression.reason),
|
|
104
|
+
email: suppression.email,
|
|
105
|
+
}
|
|
106
|
+
: { date: '', reason: '', email: send.recipientEmail }
|
|
107
|
+
if (isSuppressedStatus(send.status))
|
|
108
|
+
return suppression
|
|
109
|
+
? notice('not_sent', 'warning', true, params)
|
|
110
|
+
: notice('reactivated', 'success', false, params)
|
|
111
|
+
return suppression ? notice('on_list', 'info', true, params) : null
|
|
112
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Block,
|
|
3
|
+
FooterBlock,
|
|
4
|
+
LocaleContent,
|
|
5
|
+
TemplateContent,
|
|
6
|
+
VariableDefinition,
|
|
7
|
+
} from '../types/mailing'
|
|
8
|
+
import type { EditorCheck } from './editor-checks'
|
|
9
|
+
import {
|
|
10
|
+
PREFERENCES_URL_PATH,
|
|
11
|
+
SYSTEM_PATHS,
|
|
12
|
+
UNSUBSCRIBE_URL_PATH,
|
|
13
|
+
pathsInBlocks,
|
|
14
|
+
textPaths,
|
|
15
|
+
} from './paths'
|
|
16
|
+
|
|
17
|
+
/** The category whose e-mails are campaigns; the settings never remove it. */
|
|
18
|
+
export const MARKETING_CATEGORY = 'marketing'
|
|
19
|
+
|
|
20
|
+
type FooterLabelKey = 'unsubscribeLabel' | 'preferencesLabel'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Each footer link: the label that shows it and the URL the caller passes.
|
|
24
|
+
* The footer links are the system variables of a template's contract, as in
|
|
25
|
+
* the server's `src/engine/system-variables.ts`, which the layer cannot
|
|
26
|
+
* import: a link shows when its label is filled.
|
|
27
|
+
*/
|
|
28
|
+
export const FOOTER_LINK_PATHS: Record<FooterLabelKey, string> = {
|
|
29
|
+
unsubscribeLabel: UNSUBSCRIBE_URL_PATH,
|
|
30
|
+
preferencesLabel: PREFERENCES_URL_PATH,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const LABEL_KEYS = Object.keys(FOOTER_LINK_PATHS) as FooterLabelKey[]
|
|
34
|
+
const SYSTEM_VARIABLE_TYPE = 'url'
|
|
35
|
+
const CHECK_KEY_PREFIX = 'dms_mailing.editor.checks'
|
|
36
|
+
|
|
37
|
+
/** A system variable: declared by the content itself, never by hand. */
|
|
38
|
+
export interface SystemVariable extends VariableDefinition {
|
|
39
|
+
system: true
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function isMarketingCategory(category?: string | null): boolean {
|
|
43
|
+
return category === MARKETING_CATEGORY
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Whether a new footer of this category carries the unsubscribe and
|
|
48
|
+
* preferences links: a transactional e-mail is not subscribed to, so only an
|
|
49
|
+
* uncategorised or marketing one does.
|
|
50
|
+
*/
|
|
51
|
+
export function hasFooterLinksByDefault(category?: string | null): boolean {
|
|
52
|
+
return !category || isMarketingCategory(category)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const isFooter = (block: Block): block is FooterBlock => block.type === 'footer'
|
|
56
|
+
|
|
57
|
+
/** The URL paths a footer shows a link for: the ones with a label. */
|
|
58
|
+
export function footerLinkPaths(block: FooterBlock): string[] {
|
|
59
|
+
return LABEL_KEYS.filter((key) => block[key].trim()).map(
|
|
60
|
+
(key) => FOOTER_LINK_PATHS[key],
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function footersIn(blocks: Block[]): FooterBlock[] {
|
|
65
|
+
return blocks.flatMap((block) => {
|
|
66
|
+
if (isFooter(block)) return [block]
|
|
67
|
+
if (block.type !== 'if') return []
|
|
68
|
+
return footersIn([...block.children, ...(block.elseChildren ?? [])])
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function localeSystemPaths(locale: LocaleContent): string[] {
|
|
73
|
+
const linked = footersIn(locale.blocks).flatMap(footerLinkPaths)
|
|
74
|
+
const referenced = [
|
|
75
|
+
...textPaths(locale.subject),
|
|
76
|
+
...textPaths(locale.preheader),
|
|
77
|
+
...pathsInBlocks(locale.blocks),
|
|
78
|
+
]
|
|
79
|
+
return [...linked, ...referenced]
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** The system paths any locale of the content uses, in a stable order. */
|
|
83
|
+
export function contentSystemPaths(
|
|
84
|
+
content: Pick<TemplateContent, 'locales'>,
|
|
85
|
+
): string[] {
|
|
86
|
+
const used = new Set(
|
|
87
|
+
Object.values(content.locales ?? {}).flatMap(localeSystemPaths),
|
|
88
|
+
)
|
|
89
|
+
return SYSTEM_PATHS.filter((path) => used.has(path))
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The URLs a template's caller passes for the links its content shows, as
|
|
94
|
+
* declarations: type url, required when the template is marketing.
|
|
95
|
+
*/
|
|
96
|
+
export function systemVariables(
|
|
97
|
+
content: Pick<TemplateContent, 'locales'>,
|
|
98
|
+
category?: string | null,
|
|
99
|
+
): SystemVariable[] {
|
|
100
|
+
const required = isMarketingCategory(category)
|
|
101
|
+
return contentSystemPaths(content).map((path) => ({
|
|
102
|
+
path,
|
|
103
|
+
type: SYSTEM_VARIABLE_TYPE,
|
|
104
|
+
required,
|
|
105
|
+
system: true,
|
|
106
|
+
}))
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const hasUnsubscribeLink = (locale: LocaleContent): boolean =>
|
|
110
|
+
localeSystemPaths(locale).includes(UNSUBSCRIBE_URL_PATH)
|
|
111
|
+
|
|
112
|
+
const check = (
|
|
113
|
+
id: string,
|
|
114
|
+
key: string,
|
|
115
|
+
params: Record<string, string | number>,
|
|
116
|
+
): EditorCheck => ({
|
|
117
|
+
id,
|
|
118
|
+
state: 'warn',
|
|
119
|
+
key: `${CHECK_KEY_PREFIX}.${key}`,
|
|
120
|
+
params,
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
function unlinkedMarketingLocales(
|
|
124
|
+
content: Pick<TemplateContent, 'locales'>,
|
|
125
|
+
category?: string | null,
|
|
126
|
+
): EditorCheck[] {
|
|
127
|
+
if (!isMarketingCategory(category)) return []
|
|
128
|
+
return Object.entries(content.locales ?? {})
|
|
129
|
+
.filter(([, locale]) => !hasUnsubscribeLink(locale))
|
|
130
|
+
.map(([code]) =>
|
|
131
|
+
check(`unsubscribe-${code}`, 'marketing_no_unsubscribe', {
|
|
132
|
+
locale: code.toUpperCase(),
|
|
133
|
+
}),
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* What the publish review warns about the footer links: a marketing locale
|
|
139
|
+
* without an unsubscribe link, and a required system URL the test data does
|
|
140
|
+
* not carry — a send without it is refused or goes out without the link.
|
|
141
|
+
*/
|
|
142
|
+
export function systemVariableChecks(
|
|
143
|
+
content: Pick<TemplateContent, 'locales'>,
|
|
144
|
+
category: string | null | undefined,
|
|
145
|
+
data: Record<string, unknown>,
|
|
146
|
+
): EditorCheck[] {
|
|
147
|
+
const missing = systemVariables(content, category)
|
|
148
|
+
.filter((variable) => variable.required)
|
|
149
|
+
.filter((variable) => !hasSystemUrl(data, variable.path))
|
|
150
|
+
.map((variable) =>
|
|
151
|
+
check(`system-${variable.path}`, 'system_url_missing', {
|
|
152
|
+
path: variable.path,
|
|
153
|
+
}),
|
|
154
|
+
)
|
|
155
|
+
return [...unlinkedMarketingLocales(content, category), ...missing]
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Whether the data a canvas previews carries a usable URL for `path`. */
|
|
159
|
+
export function hasSystemUrl(
|
|
160
|
+
data: Record<string, unknown> | null | undefined,
|
|
161
|
+
path: string,
|
|
162
|
+
): boolean {
|
|
163
|
+
const value = data?.[path]
|
|
164
|
+
return typeof value === 'string' && value.trim() !== ''
|
|
165
|
+
}
|