@antelopejs/dms-mailing 0.3.4 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +232 -22
- package/dist/automation/email-event-trigger.js +2 -2
- package/dist/automation/email-event-trigger.js.map +1 -1
- package/dist/automation/send-template-action.js +2 -2
- package/dist/automation/send-template-action.js.map +1 -1
- package/dist/config.js +3 -1
- package/dist/config.js.map +1 -1
- package/dist/constants.d.ts +10 -1
- package/dist/constants.js +15 -3
- package/dist/constants.js.map +1 -1
- package/dist/crons/attention.d.ts +3 -0
- package/dist/crons/attention.js +19 -0
- package/dist/crons/attention.js.map +1 -0
- package/dist/crons/index.d.ts +2 -0
- package/dist/crons/index.js +2 -0
- package/dist/crons/index.js.map +1 -1
- package/dist/crons/jobs.d.ts +12 -0
- package/dist/crons/jobs.js +27 -0
- package/dist/crons/jobs.js.map +1 -0
- package/dist/crons/retention.d.ts +2 -4
- package/dist/crons/retention.js +7 -22
- package/dist/crons/retention.js.map +1 -1
- package/dist/data/displays.d.ts +5 -0
- package/dist/data/displays.js +23 -0
- package/dist/data/displays.js.map +1 -0
- package/dist/data/index.d.ts +1 -0
- package/dist/data/index.js +1 -0
- package/dist/data/index.js.map +1 -1
- package/dist/data/locale-fallback.d.ts +3 -0
- package/dist/data/locale-fallback.js +20 -0
- package/dist/data/locale-fallback.js.map +1 -0
- package/dist/data/read-only-routes.d.ts +7 -0
- package/dist/data/read-only-routes.js +13 -0
- package/dist/data/read-only-routes.js.map +1 -0
- package/dist/data/sends-table.d.ts +19 -5
- package/dist/data/sends-table.js +185 -17
- package/dist/data/sends-table.js.map +1 -1
- package/dist/data/suppressions-table.d.ts +24 -0
- package/dist/data/suppressions-table.js +120 -0
- package/dist/data/suppressions-table.js.map +1 -0
- package/dist/data/templates-table.d.ts +20 -2
- package/dist/data/templates-table.js +188 -31
- package/dist/data/templates-table.js.map +1 -1
- package/dist/data-types/categories-type.d.ts +11 -13
- package/dist/data-types/categories-type.js +3 -3
- package/dist/data-types/categories-type.js.map +1 -1
- package/dist/data-types/category-type.js +1 -1
- package/dist/data-types/category-type.js.map +1 -1
- package/dist/db/models/index.d.ts +3 -0
- package/dist/db/models/index.js +3 -0
- package/dist/db/models/index.js.map +1 -1
- package/dist/db/models/sends.model.d.ts +12 -1
- package/dist/db/models/sends.model.js +64 -0
- package/dist/db/models/sends.model.js.map +1 -1
- package/dist/db/models/settings.model.d.ts +0 -1
- package/dist/db/models/settings.model.js +0 -7
- package/dist/db/models/settings.model.js.map +1 -1
- package/dist/db/models/suppressions.model.d.ts +28 -0
- package/dist/db/models/suppressions.model.js +46 -0
- package/dist/db/models/suppressions.model.js.map +1 -0
- package/dist/db/models/template-versions.model.d.ts +24 -0
- package/dist/db/models/template-versions.model.js +17 -0
- package/dist/db/models/template-versions.model.js.map +1 -0
- package/dist/db/models/templates.model.d.ts +2 -0
- package/dist/db/models/templates.model.js +3 -0
- package/dist/db/models/templates.model.js.map +1 -1
- package/dist/db/models/webhook-status.model.d.ts +24 -0
- package/dist/db/models/webhook-status.model.js +21 -0
- package/dist/db/models/webhook-status.model.js.map +1 -0
- package/dist/db/tables/index.d.ts +3 -0
- package/dist/db/tables/index.js +3 -0
- package/dist/db/tables/index.js.map +1 -1
- package/dist/db/tables/send-events.table.d.ts +2 -2
- package/dist/db/tables/send-events.table.js.map +1 -1
- package/dist/db/tables/sends.table.d.ts +10 -2
- package/dist/db/tables/sends.table.js +34 -1
- package/dist/db/tables/sends.table.js.map +1 -1
- package/dist/db/tables/suppressions.table.d.ts +12 -0
- package/dist/db/tables/suppressions.table.js +53 -0
- package/dist/db/tables/suppressions.table.js.map +1 -0
- package/dist/db/tables/template-versions.table.d.ts +13 -0
- package/dist/db/tables/template-versions.table.js +59 -0
- package/dist/db/tables/template-versions.table.js.map +1 -0
- package/dist/db/tables/templates.table.d.ts +11 -0
- package/dist/db/tables/templates.table.js +45 -0
- package/dist/db/tables/templates.table.js.map +1 -1
- package/dist/db/tables/webhook-status.table.d.ts +14 -0
- package/dist/db/tables/webhook-status.table.js +58 -0
- package/dist/db/tables/webhook-status.table.js.map +1 -0
- package/dist/engine/changes.d.ts +12 -0
- package/dist/engine/changes.js +107 -0
- package/dist/engine/changes.js.map +1 -0
- package/dist/engine/conditions.js +0 -2
- package/dist/engine/conditions.js.map +1 -1
- package/dist/engine/coverage.d.ts +1 -0
- package/dist/engine/coverage.js +6 -2
- package/dist/engine/coverage.js.map +1 -1
- package/dist/engine/fallback-html.d.ts +1 -1
- package/dist/engine/fallback-html.js +51 -34
- package/dist/engine/fallback-html.js.map +1 -1
- package/dist/engine/index.d.ts +3 -0
- package/dist/engine/index.js +3 -0
- package/dist/engine/index.js.map +1 -1
- package/dist/engine/plain-text.d.ts +3 -0
- package/dist/engine/plain-text.js +60 -0
- package/dist/engine/plain-text.js.map +1 -0
- package/dist/engine/resolve.d.ts +2 -1
- package/dist/engine/resolve.js +17 -9
- package/dist/engine/resolve.js.map +1 -1
- package/dist/engine/system-variables.d.ts +10 -0
- package/dist/engine/system-variables.js +70 -0
- package/dist/engine/system-variables.js.map +1 -0
- package/dist/engine/tokens.d.ts +2 -1
- package/dist/engine/tokens.js +6 -2
- package/dist/engine/tokens.js.map +1 -1
- package/dist/hooks/tenant-export.js +13 -1
- package/dist/hooks/tenant-export.js.map +1 -1
- package/dist/implementations/dms-mailing/index.d.ts +1 -1
- package/dist/implementations/dms-mailing/index.js +2 -2
- package/dist/implementations/dms-mailing/index.js.map +1 -1
- package/dist/index.js +13 -11
- package/dist/index.js.map +1 -1
- package/dist/pages/editor.js +9 -2
- package/dist/pages/editor.js.map +1 -1
- package/dist/pages/index.d.ts +1 -0
- package/dist/pages/index.js +1 -0
- package/dist/pages/index.js.map +1 -1
- package/dist/pages/module.d.ts +2 -0
- package/dist/pages/module.js +26 -2
- package/dist/pages/module.js.map +1 -1
- package/dist/pages/overview.d.ts +2 -6
- package/dist/pages/overview.js +109 -52
- package/dist/pages/overview.js.map +1 -1
- package/dist/pages/sends.d.ts +3 -4
- package/dist/pages/sends.js +197 -60
- package/dist/pages/sends.js.map +1 -1
- package/dist/pages/settings/form.js +114 -53
- package/dist/pages/settings/form.js.map +1 -1
- package/dist/pages/settings/page.d.ts +0 -1
- package/dist/pages/settings/page.js +8 -11
- package/dist/pages/settings/page.js.map +1 -1
- package/dist/pages/suppressions.d.ts +9 -0
- package/dist/pages/suppressions.js +151 -0
- package/dist/pages/suppressions.js.map +1 -0
- package/dist/pages/templates.d.ts +0 -1
- package/dist/pages/templates.js +115 -41
- package/dist/pages/templates.js.map +1 -1
- package/dist/permissions.js +2 -2
- package/dist/permissions.js.map +1 -1
- package/dist/quick-actions.js +7 -4
- package/dist/quick-actions.js.map +1 -1
- package/dist/routes/events.d.ts +8 -5
- package/dist/routes/events.js +55 -24
- package/dist/routes/events.js.map +1 -1
- package/dist/routes/index.d.ts +2 -0
- package/dist/routes/index.js +2 -0
- package/dist/routes/index.js.map +1 -1
- package/dist/routes/metrics.d.ts +60 -23
- package/dist/routes/metrics.js +231 -83
- package/dist/routes/metrics.js.map +1 -1
- package/dist/routes/period.d.ts +2 -1
- package/dist/routes/period.js +9 -3
- package/dist/routes/period.js.map +1 -1
- package/dist/routes/sends.d.ts +49 -8
- package/dist/routes/sends.js +239 -24
- package/dist/routes/sends.js.map +1 -1
- package/dist/routes/settings.d.ts +44 -5
- package/dist/routes/settings.js +190 -15
- package/dist/routes/settings.js.map +1 -1
- package/dist/routes/suppressions.d.ts +16 -0
- package/dist/routes/suppressions.js +92 -0
- package/dist/routes/suppressions.js.map +1 -0
- package/dist/routes/template-versions.d.ts +28 -0
- package/dist/routes/template-versions.js +87 -0
- package/dist/routes/template-versions.js.map +1 -0
- package/dist/routes/templates.d.ts +60 -6
- package/dist/routes/templates.js +204 -35
- package/dist/routes/templates.js.map +1 -1
- package/dist/services/attention.d.ts +19 -6
- package/dist/services/attention.js +177 -12
- package/dist/services/attention.js.map +1 -1
- package/dist/services/catalog.d.ts +3 -0
- package/dist/services/catalog.js +25 -0
- package/dist/services/catalog.js.map +1 -0
- package/dist/services/categories.d.ts +8 -32
- package/dist/services/categories.js +14 -25
- package/dist/services/categories.js.map +1 -1
- package/dist/services/checked-send.d.ts +9 -0
- package/dist/services/checked-send.js +54 -0
- package/dist/services/checked-send.js.map +1 -0
- package/dist/services/events.d.ts +5 -0
- package/dist/services/events.js +102 -9
- package/dist/services/events.js.map +1 -1
- package/dist/services/metrics.d.ts +17 -6
- package/dist/services/metrics.js +57 -18
- package/dist/services/metrics.js.map +1 -1
- package/dist/services/notify.d.ts +18 -10
- package/dist/services/notify.js +82 -37
- package/dist/services/notify.js.map +1 -1
- package/dist/services/provider-banner.d.ts +14 -0
- package/dist/services/provider-banner.js +115 -0
- package/dist/services/provider-banner.js.map +1 -0
- package/dist/services/render.js.map +1 -1
- package/dist/services/send-stat-items.d.ts +7 -0
- package/dist/services/send-stat-items.js +127 -0
- package/dist/services/send-stat-items.js.map +1 -0
- package/dist/services/send-stats.d.ts +23 -0
- package/dist/services/send-stats.js +63 -0
- package/dist/services/send-stats.js.map +1 -0
- package/dist/services/send.d.ts +18 -3
- package/dist/services/send.js +81 -17
- package/dist/services/send.js.map +1 -1
- package/dist/services/settings.d.ts +5 -0
- package/dist/services/settings.js +28 -7
- package/dist/services/settings.js.map +1 -1
- package/dist/services/starters.d.ts +15 -0
- package/dist/services/starters.js +264 -0
- package/dist/services/starters.js.map +1 -0
- package/dist/services/suppressions.d.ts +20 -0
- package/dist/services/suppressions.js +53 -0
- package/dist/services/suppressions.js.map +1 -0
- package/dist/services/template-attention.d.ts +4 -0
- package/dist/services/template-attention.js +43 -0
- package/dist/services/template-attention.js.map +1 -0
- package/dist/services/template-stats.d.ts +27 -0
- package/dist/services/template-stats.js +59 -0
- package/dist/services/template-stats.js.map +1 -0
- package/dist/services/templates.d.ts +31 -4
- package/dist/services/templates.js +240 -21
- package/dist/services/templates.js.map +1 -1
- package/dist/services/webhook-status.d.ts +27 -0
- package/dist/services/webhook-status.js +134 -0
- package/dist/services/webhook-status.js.map +1 -0
- package/dist/types/blocks.d.ts +1 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -1
- package/dist/types/sends.d.ts +10 -1
- package/dist/types/sends.js +35 -2
- package/dist/types/sends.js.map +1 -1
- package/dist/types/suppressions.d.ts +9 -0
- package/dist/types/suppressions.js +10 -0
- package/dist/types/suppressions.js.map +1 -0
- package/dist/types/webhook.d.ts +17 -0
- package/dist/types/webhook.js +3 -0
- package/dist/types/webhook.js.map +1 -0
- package/dist/validation/settings.schema.d.ts +144 -9
- package/dist/validation/settings.schema.js +113 -16
- package/dist/validation/settings.schema.js.map +1 -1
- package/dist/validation/templates.schema.d.ts +137 -10
- package/dist/validation/templates.schema.js +28 -2
- package/dist/validation/templates.schema.js.map +1 -1
- package/frontend-vue/app/build/components/FormFieldRow.vue +90 -0
- package/frontend-vue/app/build/components/FormRows.vue +26 -0
- package/frontend-vue/app/build/components/RequiredLegend.vue +11 -0
- package/frontend-vue/app/components/CategoriesInput.vue +323 -77
- package/frontend-vue/app/components/CategoryInput.vue +13 -14
- package/frontend-vue/app/components/CategoryLabel.vue +34 -0
- package/frontend-vue/app/components/DuplicateModal.vue +201 -0
- package/frontend-vue/app/components/Editor.vue +55 -20
- package/frontend-vue/app/components/FixAddressModal.vue +133 -0
- package/frontend-vue/app/components/FunnelCard.vue +146 -43
- package/frontend-vue/app/components/LocaleSelect.vue +98 -0
- package/frontend-vue/app/components/NewTemplateModal.vue +388 -102
- package/frontend-vue/app/components/NewTemplateSourceTile.vue +70 -0
- package/frontend-vue/app/components/OverviewGate.vue +220 -0
- package/frontend-vue/app/components/PageHeader.vue +98 -0
- package/frontend-vue/app/components/RealSendModal.vue +555 -0
- package/frontend-vue/app/components/RecipientsField.vue +41 -0
- package/frontend-vue/app/components/RenderingModal.vue +189 -0
- package/frontend-vue/app/components/RetentionInput.vue +212 -0
- package/frontend-vue/app/components/SendDrawer.vue +542 -209
- package/frontend-vue/app/components/StarterGallery.vue +153 -0
- package/frontend-vue/app/components/SystemVariables.vue +63 -0
- package/frontend-vue/app/components/TemplateCard.vue +218 -57
- package/frontend-vue/app/components/TemplateDrawer.vue +785 -274
- package/frontend-vue/app/components/TemplateGallery.vue +312 -56
- package/frontend-vue/app/components/TemplatePreviewFrame.vue +112 -22
- package/frontend-vue/app/components/TestSendModal.vue +266 -95
- package/frontend-vue/app/components/TokenText.vue +61 -10
- package/frontend-vue/app/components/VariablePath.vue +20 -0
- package/frontend-vue/app/components/VersionHistory.vue +196 -0
- package/frontend-vue/app/components/VersionPreview.vue +164 -0
- package/frontend-vue/app/components/WebhookStatus.vue +130 -0
- package/frontend-vue/app/components/blocks/Button.vue +1 -1
- package/frontend-vue/app/components/blocks/Code.vue +1 -1
- package/frontend-vue/app/components/blocks/Footer.vue +56 -7
- package/frontend-vue/app/components/blocks/Heading.vue +1 -1
- package/frontend-vue/app/components/blocks/Hero.vue +13 -7
- package/frontend-vue/app/components/blocks/List.vue +50 -6
- package/frontend-vue/app/components/blocks/Paragraph.vue +1 -1
- package/frontend-vue/app/components/blocks/Total.vue +3 -3
- package/frontend-vue/app/components/editor/EditorAutocompleteMenu.vue +50 -0
- package/frontend-vue/app/components/editor/EditorBlock.vue +37 -90
- package/frontend-vue/app/components/editor/EditorBlockFields.vue +140 -0
- package/frontend-vue/app/components/editor/EditorBlockInspector.vue +92 -0
- package/frontend-vue/app/components/editor/EditorBlockList.vue +32 -25
- package/frontend-vue/app/components/editor/EditorBlockPicker.vue +58 -0
- package/frontend-vue/app/components/editor/EditorBlockTools.vue +71 -0
- package/frontend-vue/app/components/editor/EditorCanvas.vue +105 -70
- package/frontend-vue/app/components/editor/EditorCompare.vue +211 -0
- package/frontend-vue/app/components/editor/EditorConditionFields.vue +56 -46
- package/frontend-vue/app/components/editor/EditorConditionInspector.vue +54 -0
- package/frontend-vue/app/components/editor/EditorEnvelope.vue +77 -0
- package/frontend-vue/app/components/editor/EditorIfBlock.vue +36 -86
- package/frontend-vue/app/components/editor/EditorInsertPoint.vue +64 -0
- package/frontend-vue/app/components/editor/EditorInspector.vue +80 -0
- package/frontend-vue/app/components/editor/EditorLeftPane.vue +103 -0
- package/frontend-vue/app/components/editor/EditorLifecycle.vue +67 -0
- package/frontend-vue/app/components/editor/EditorLoading.vue +23 -0
- package/frontend-vue/app/components/editor/EditorLocaleSwitch.vue +61 -0
- package/frontend-vue/app/components/editor/EditorMissingLocale.vue +72 -48
- package/frontend-vue/app/components/editor/EditorMissingPolicy.vue +42 -0
- package/frontend-vue/app/components/editor/EditorNotice.vue +127 -0
- package/frontend-vue/app/components/editor/EditorOutline.vue +65 -0
- package/frontend-vue/app/components/editor/EditorPalette.vue +95 -42
- package/frontend-vue/app/components/editor/EditorPaletteGroup.vue +66 -0
- package/frontend-vue/app/components/editor/EditorPublishReview.vue +102 -0
- package/frontend-vue/app/components/editor/EditorSaveState.vue +88 -0
- package/frontend-vue/app/components/editor/EditorShortcutHint.vue +17 -0
- package/frontend-vue/app/components/editor/EditorSimChip.vue +51 -0
- package/frontend-vue/app/components/editor/EditorTemplatePane.vue +131 -0
- package/frontend-vue/app/components/editor/EditorTestDataPane.vue +98 -0
- package/frontend-vue/app/components/editor/EditorTitle.vue +82 -0
- package/frontend-vue/app/components/editor/EditorTokenField.vue +224 -0
- package/frontend-vue/app/components/editor/EditorToolbar.vue +34 -134
- package/frontend-vue/app/components/editor/EditorVariableForm.vue +100 -0
- package/frontend-vue/app/components/editor/EditorVariableRow.vue +87 -0
- package/frontend-vue/app/components/editor/EditorVariablesPane.vue +161 -0
- package/frontend-vue/app/components/editor/EditorViewSwitches.vue +76 -0
- package/frontend-vue/app/components/editor/EditorVisibility.vue +101 -0
- package/frontend-vue/app/components/editor/EditorWorkspace.vue +62 -149
- package/frontend-vue/app/composables/useBlockContext.ts +16 -0
- package/frontend-vue/app/composables/useEditorActions.ts +381 -0
- package/frontend-vue/app/composables/useEditorAnnouncements.ts +51 -0
- package/frontend-vue/app/composables/useEditorContext.ts +30 -23
- package/frontend-vue/app/composables/useEditorSession.ts +299 -0
- package/frontend-vue/app/composables/useEditorShortcuts.ts +81 -0
- package/frontend-vue/app/composables/useLocalizedDefaults.ts +17 -0
- package/frontend-vue/app/composables/useMailingApi.ts +142 -27
- package/frontend-vue/app/composables/useOverviewSetup.ts +13 -0
- package/frontend-vue/app/composables/useReplayToast.ts +72 -0
- package/frontend-vue/app/composables/useTemplateCategories.ts +35 -0
- package/frontend-vue/app/composables/useTemplateDrawer.ts +196 -62
- package/frontend-vue/app/composables/useTemplateEditor.ts +318 -86
- package/frontend-vue/app/emails/EmailMailingTemplate.vue +16 -4
- package/frontend-vue/app/emails/components/MailingBlocks.vue +96 -38
- package/frontend-vue/app/emails/components/MailingTextLines.vue +20 -0
- package/frontend-vue/app/plugins/column-displays.ts +65 -0
- package/frontend-vue/app/plugins/{template-gallery-display.client.ts → template-gallery-display.ts} +1 -1
- package/frontend-vue/app/types/mailing.ts +193 -18
- package/frontend-vue/app/utils/api-error.ts +33 -0
- package/frontend-vue/app/utils/autocomplete.ts +120 -0
- package/frontend-vue/app/utils/blocks/components.ts +10 -10
- package/frontend-vue/app/utils/blocks/dnd.ts +7 -3
- package/frontend-vue/app/utils/blocks/factory.ts +199 -38
- package/frontend-vue/app/utils/blocks/fields.ts +39 -19
- package/frontend-vue/app/utils/blocks/index.ts +1 -0
- package/frontend-vue/app/utils/blocks/outline.ts +91 -0
- package/frontend-vue/app/utils/blocks/tree.ts +114 -2
- package/frontend-vue/app/utils/categories.ts +201 -15
- package/frontend-vue/app/utils/changes.ts +152 -0
- package/frontend-vue/app/utils/conditions.ts +25 -19
- package/frontend-vue/app/utils/editor-checks.ts +266 -0
- package/frontend-vue/app/utils/editor-route.ts +43 -4
- package/frontend-vue/app/utils/funnel.ts +74 -0
- package/frontend-vue/app/utils/gallery.ts +179 -1
- package/frontend-vue/app/utils/history.ts +66 -0
- package/frontend-vue/app/utils/lifecycle.ts +185 -0
- package/frontend-vue/app/utils/locales.ts +107 -2
- package/frontend-vue/app/utils/overview-setup.ts +83 -0
- package/frontend-vue/app/utils/pages.ts +5 -0
- package/frontend-vue/app/utils/paths.ts +51 -15
- package/frontend-vue/app/utils/period.ts +62 -22
- package/frontend-vue/app/utils/problems.ts +181 -0
- package/frontend-vue/app/utils/recipients.ts +49 -0
- package/frontend-vue/app/utils/rendering-title.ts +23 -0
- package/frontend-vue/app/utils/send-actions.ts +207 -0
- package/frontend-vue/app/utils/send-link.ts +28 -0
- package/frontend-vue/app/utils/send-snippet.ts +97 -0
- package/frontend-vue/app/utils/send-stats.ts +24 -0
- package/frontend-vue/app/utils/send-values.ts +156 -0
- package/frontend-vue/app/utils/shortcuts.ts +113 -0
- package/frontend-vue/app/utils/slug.ts +82 -0
- package/frontend-vue/app/utils/starters.ts +23 -0
- package/frontend-vue/app/utils/suppression.ts +112 -0
- package/frontend-vue/app/utils/system-variables.ts +165 -0
- package/frontend-vue/app/utils/template-attention.ts +104 -0
- package/frontend-vue/app/utils/timeline.ts +221 -19
- package/frontend-vue/app/utils/tokens.ts +115 -13
- package/frontend-vue/app/utils/variable-types.ts +23 -0
- package/frontend-vue/app/utils/variables.ts +208 -1
- package/frontend-vue/app/utils/version-history.ts +53 -0
- package/frontend-vue/app/utils/webhook-status.ts +170 -0
- package/frontend-vue/dms.frontend.ts +19 -11
- package/frontend-vue/i18n/locales/mailing-en-GB.json +1173 -161
- package/frontend-vue/i18n/locales/mailing-fr-FR.json +1220 -208
- package/frontend-vue/package.json +2 -2
- package/frontend-vue/pnpm-lock.yaml +46 -161
- package/frontend-vue/tests/api-error.test.ts +27 -0
- package/frontend-vue/tests/autocomplete.test.ts +84 -0
- package/frontend-vue/tests/block-components.test.ts +0 -3
- package/frontend-vue/tests/block-fields.test.ts +12 -2
- package/frontend-vue/tests/blocks-factory.test.ts +46 -0
- package/frontend-vue/tests/blocks-outline.test.ts +102 -0
- package/frontend-vue/tests/blocks-tree.test.ts +28 -1
- package/frontend-vue/tests/categories-input.test.ts +140 -0
- package/frontend-vue/tests/categories.test.ts +148 -15
- package/frontend-vue/tests/changes.test.ts +136 -0
- package/frontend-vue/tests/column-displays.test.ts +73 -0
- package/frontend-vue/tests/conditions.test.ts +49 -21
- package/frontend-vue/tests/dnd.test.ts +10 -0
- package/frontend-vue/tests/editor-block-list.test.ts +25 -2
- package/frontend-vue/tests/editor-checks.test.ts +178 -0
- package/frontend-vue/tests/editor-route.test.ts +37 -9
- package/frontend-vue/tests/editor-session.test.ts +207 -0
- package/frontend-vue/tests/footer-block.test.ts +89 -0
- package/frontend-vue/tests/funnel-card.test.ts +117 -0
- package/frontend-vue/tests/funnel.test.ts +81 -0
- package/frontend-vue/tests/gallery.test.ts +124 -2
- package/frontend-vue/tests/helpers/nuxt-globals.ts +232 -0
- package/frontend-vue/tests/history.test.ts +48 -0
- package/frontend-vue/tests/i18n-editor.test.ts +37 -8
- package/frontend-vue/tests/i18n.test.ts +55 -0
- package/frontend-vue/tests/lifecycle.test.ts +145 -0
- package/frontend-vue/tests/locales.test.ts +60 -0
- package/frontend-vue/tests/localized-defaults.test.ts +54 -0
- package/frontend-vue/tests/new-template-modal.test.ts +211 -0
- package/frontend-vue/tests/overview-gate.test.ts +147 -0
- package/frontend-vue/tests/overview-setup.test.ts +78 -0
- package/frontend-vue/tests/paths.test.ts +23 -0
- package/frontend-vue/tests/period.test.ts +59 -10
- package/frontend-vue/tests/problems.test.ts +153 -0
- package/frontend-vue/tests/recipients.test.ts +47 -0
- package/frontend-vue/tests/rendering-title.test.ts +24 -0
- package/frontend-vue/tests/replay-toast.test.ts +67 -0
- package/frontend-vue/tests/retention-input.test.ts +130 -0
- package/frontend-vue/tests/send-actions.test.ts +128 -0
- package/frontend-vue/tests/send-modals.test.ts +416 -0
- package/frontend-vue/tests/send-snippet.test.ts +51 -0
- package/frontend-vue/tests/send-stats.test.ts +12 -0
- package/frontend-vue/tests/send-values.test.ts +119 -0
- package/frontend-vue/tests/shortcuts.test.ts +76 -0
- package/frontend-vue/tests/slug.test.ts +88 -0
- package/frontend-vue/tests/suppression.test.ts +159 -0
- package/frontend-vue/tests/system-variables.test.ts +133 -0
- package/frontend-vue/tests/template-attention.test.ts +123 -0
- package/frontend-vue/tests/template-drawer.test.ts +450 -0
- package/frontend-vue/tests/template-editor.test.ts +218 -9
- package/frontend-vue/tests/template-gallery-display.test.ts +32 -0
- package/frontend-vue/tests/template-gallery.test.ts +371 -0
- package/frontend-vue/tests/template-overlays.test.ts +125 -0
- package/frontend-vue/tests/timeline.test.ts +175 -20
- package/frontend-vue/tests/tokens.test.ts +79 -1
- package/frontend-vue/tests/variable-path.test.ts +23 -0
- package/frontend-vue/tests/variable-types.test.ts +20 -0
- package/frontend-vue/tests/variables.test.ts +90 -2
- package/frontend-vue/tests/version-history.test.ts +41 -0
- package/frontend-vue/tests/webhook-status.test.ts +108 -0
- package/frontend-vue/vitest.config.ts +4 -1
- package/package.json +14 -12
- package/src/automation/email-event-trigger.ts +3 -3
- package/src/automation/send-template-action.ts +7 -5
- package/src/config.ts +3 -1
- package/src/constants.ts +18 -2
- package/src/crons/attention.ts +21 -0
- package/src/crons/index.ts +2 -0
- package/src/crons/jobs.ts +35 -0
- package/src/crons/retention.ts +7 -22
- package/src/data/displays.ts +18 -0
- package/src/data/index.ts +1 -0
- package/src/data/locale-fallback.ts +26 -0
- package/src/data/read-only-routes.ts +14 -0
- package/src/data/sends-table.ts +204 -19
- package/src/data/suppressions-table.ts +127 -0
- package/src/data/templates-table.ts +295 -63
- package/src/data-types/categories-type.ts +4 -5
- package/src/data-types/category-type.ts +1 -1
- package/src/db/models/index.ts +3 -0
- package/src/db/models/sends.model.ts +102 -1
- package/src/db/models/settings.model.ts +2 -12
- package/src/db/models/suppressions.model.ts +53 -0
- package/src/db/models/template-versions.model.ts +23 -0
- package/src/db/models/templates.model.ts +17 -0
- package/src/db/models/webhook-status.model.ts +25 -0
- package/src/db/tables/index.ts +3 -0
- package/src/db/tables/send-events.table.ts +6 -2
- package/src/db/tables/sends.table.ts +37 -2
- package/src/db/tables/suppressions.table.ts +46 -0
- package/src/db/tables/template-versions.table.ts +44 -0
- package/src/db/tables/templates.table.ts +67 -5
- package/src/db/tables/webhook-status.table.ts +44 -0
- package/src/engine/changes.ts +198 -0
- package/src/engine/conditions.ts +1 -7
- package/src/engine/coverage.ts +7 -2
- package/src/engine/fallback-html.ts +80 -42
- package/src/engine/index.ts +3 -0
- package/src/engine/plain-text.ts +86 -0
- package/src/engine/resolve.ts +49 -15
- package/src/engine/system-variables.ts +124 -0
- package/src/engine/tokens.ts +13 -1
- package/src/hooks/tenant-export.ts +21 -2
- package/src/implementations/dms-mailing/index.ts +1 -1
- package/src/index.ts +29 -13
- package/src/pages/editor.ts +13 -2
- package/src/pages/index.ts +6 -3
- package/src/pages/module.ts +31 -2
- package/src/pages/overview.ts +187 -128
- package/src/pages/sends.ts +271 -112
- package/src/pages/settings/form.ts +144 -54
- package/src/pages/settings/page.ts +9 -17
- package/src/pages/suppressions.ts +157 -0
- package/src/pages/templates.ts +140 -43
- package/src/permissions.ts +6 -4
- package/src/quick-actions.ts +12 -5
- package/src/routes/events.ts +110 -33
- package/src/routes/index.ts +2 -0
- package/src/routes/metrics.ts +348 -98
- package/src/routes/period.ts +23 -6
- package/src/routes/sends.ts +413 -41
- package/src/routes/settings.ts +291 -26
- package/src/routes/suppressions.ts +87 -0
- package/src/routes/template-versions.ts +116 -0
- package/src/routes/templates.ts +334 -48
- package/src/services/attention.ts +283 -30
- package/src/services/catalog.ts +28 -0
- package/src/services/categories.ts +28 -27
- package/src/services/checked-send.ts +123 -0
- package/src/services/events.ts +174 -20
- package/src/services/metrics.ts +137 -28
- package/src/services/notify.ts +126 -44
- package/src/services/provider-banner.ts +165 -0
- package/src/services/render.ts +1 -0
- package/src/services/send-stat-items.ts +160 -0
- package/src/services/send-stats.ts +122 -0
- package/src/services/send.ts +193 -31
- package/src/services/settings.ts +52 -8
- package/src/services/starters.ts +383 -0
- package/src/services/suppressions.ts +116 -0
- package/src/services/template-attention.ts +82 -0
- package/src/services/template-stats.ts +97 -0
- package/src/services/templates.ts +400 -37
- package/src/services/webhook-status.ts +202 -0
- package/src/types/blocks.ts +6 -0
- package/src/types/index.ts +2 -0
- package/src/types/sends.ts +64 -2
- package/src/types/suppressions.ts +27 -0
- package/src/types/webhook.ts +29 -0
- package/src/validation/settings.schema.ts +173 -17
- package/src/validation/templates.schema.ts +53 -1
- package/dist/antelope-modules.d.ts +0 -32
- package/dist/antelope-modules.js +0 -36
- package/dist/antelope-modules.js.map +0 -1
- package/dist/test/antelope.test.d.ts +0 -2
- package/dist/test/antelope.test.js +0 -180
- package/dist/test/antelope.test.js.map +0 -1
- package/dist/test/helpers/http.d.ts +0 -4
- package/dist/test/helpers/http.js +0 -28
- package/dist/test/helpers/http.js.map +0 -1
- package/dist/test/helpers/owner.d.ts +0 -5
- package/dist/test/helpers/owner.js +0 -30
- package/dist/test/helpers/owner.js.map +0 -1
- package/dist/test/integration/events.test.d.ts +0 -1
- package/dist/test/integration/events.test.js +0 -143
- package/dist/test/integration/events.test.js.map +0 -1
- package/dist/test/integration/retention.test.d.ts +0 -1
- package/dist/test/integration/retention.test.js +0 -291
- package/dist/test/integration/retention.test.js.map +0 -1
- package/dist/test/integration/sends.test.d.ts +0 -1
- package/dist/test/integration/sends.test.js +0 -210
- package/dist/test/integration/sends.test.js.map +0 -1
- package/dist/test/integration/settings.test.d.ts +0 -1
- package/dist/test/integration/settings.test.js +0 -137
- package/dist/test/integration/settings.test.js.map +0 -1
- package/dist/test/integration/setup.test.d.ts +0 -1
- package/dist/test/integration/setup.test.js +0 -32
- package/dist/test/integration/setup.test.js.map +0 -1
- package/dist/test/integration/templates.test.d.ts +0 -1
- package/dist/test/integration/templates.test.js +0 -233
- package/dist/test/integration/templates.test.js.map +0 -1
- package/dist/test/integration/tenant-export.test.d.ts +0 -1
- package/dist/test/integration/tenant-export.test.js +0 -85
- package/dist/test/integration/tenant-export.test.js.map +0 -1
- package/dist/test/unit/attention.test.d.ts +0 -1
- package/dist/test/unit/attention.test.js +0 -109
- package/dist/test/unit/attention.test.js.map +0 -1
- package/dist/test/unit/automation.test.d.ts +0 -1
- package/dist/test/unit/automation.test.js +0 -104
- package/dist/test/unit/automation.test.js.map +0 -1
- package/dist/test/unit/categories.test.d.ts +0 -1
- package/dist/test/unit/categories.test.js +0 -38
- package/dist/test/unit/categories.test.js.map +0 -1
- package/dist/test/unit/engine/conditions.test.d.ts +0 -1
- package/dist/test/unit/engine/conditions.test.js +0 -23
- package/dist/test/unit/engine/conditions.test.js.map +0 -1
- package/dist/test/unit/engine/coverage.test.d.ts +0 -1
- package/dist/test/unit/engine/coverage.test.js +0 -106
- package/dist/test/unit/engine/coverage.test.js.map +0 -1
- package/dist/test/unit/engine/fallback-html.test.d.ts +0 -1
- package/dist/test/unit/engine/fallback-html.test.js +0 -78
- package/dist/test/unit/engine/fallback-html.test.js.map +0 -1
- package/dist/test/unit/engine/paths.test.d.ts +0 -1
- package/dist/test/unit/engine/paths.test.js +0 -33
- package/dist/test/unit/engine/paths.test.js.map +0 -1
- package/dist/test/unit/engine/resolve.test.d.ts +0 -1
- package/dist/test/unit/engine/resolve.test.js +0 -157
- package/dist/test/unit/engine/resolve.test.js.map +0 -1
- package/dist/test/unit/engine/tokens.test.d.ts +0 -1
- package/dist/test/unit/engine/tokens.test.js +0 -26
- package/dist/test/unit/engine/tokens.test.js.map +0 -1
- package/dist/test/unit/events.test.d.ts +0 -1
- package/dist/test/unit/events.test.js +0 -13
- package/dist/test/unit/events.test.js.map +0 -1
- package/dist/test/unit/metrics.test.d.ts +0 -1
- package/dist/test/unit/metrics.test.js +0 -108
- package/dist/test/unit/metrics.test.js.map +0 -1
- package/dist/test/unit/notify.test.d.ts +0 -1
- package/dist/test/unit/notify.test.js +0 -49
- package/dist/test/unit/notify.test.js.map +0 -1
- package/dist/test/unit/period.test.d.ts +0 -1
- package/dist/test/unit/period.test.js +0 -61
- package/dist/test/unit/period.test.js.map +0 -1
- package/dist/test/unit/permissions.test.d.ts +0 -1
- package/dist/test/unit/permissions.test.js +0 -27
- package/dist/test/unit/permissions.test.js.map +0 -1
- package/dist/test/unit/provider.test.d.ts +0 -1
- package/dist/test/unit/provider.test.js +0 -47
- package/dist/test/unit/provider.test.js.map +0 -1
- package/dist/test/unit/realtime.test.d.ts +0 -1
- package/dist/test/unit/realtime.test.js +0 -13
- package/dist/test/unit/realtime.test.js.map +0 -1
- package/dist/test/unit/render.test.d.ts +0 -1
- package/dist/test/unit/render.test.js +0 -23
- package/dist/test/unit/render.test.js.map +0 -1
- package/dist/test/unit/retention.test.d.ts +0 -1
- package/dist/test/unit/retention.test.js +0 -66
- package/dist/test/unit/retention.test.js.map +0 -1
- package/dist/test/unit/send-batch.test.d.ts +0 -1
- package/dist/test/unit/send-batch.test.js +0 -46
- package/dist/test/unit/send-batch.test.js.map +0 -1
- package/dist/test/unit/send.test.d.ts +0 -1
- package/dist/test/unit/send.test.js +0 -120
- package/dist/test/unit/send.test.js.map +0 -1
- package/dist/test/unit/smoke.test.d.ts +0 -1
- package/dist/test/unit/smoke.test.js +0 -10
- package/dist/test/unit/smoke.test.js.map +0 -1
- package/dist/test/unit/status.test.d.ts +0 -1
- package/dist/test/unit/status.test.js +0 -16
- package/dist/test/unit/status.test.js.map +0 -1
- package/dist/test/unit/templates.test.d.ts +0 -1
- package/dist/test/unit/templates.test.js +0 -48
- package/dist/test/unit/templates.test.js.map +0 -1
- package/dist/test/unit/tenant-export.test.d.ts +0 -1
- package/dist/test/unit/tenant-export.test.js +0 -59
- package/dist/test/unit/tenant-export.test.js.map +0 -1
- package/dist/test/unit/webhook-secret.test.d.ts +0 -1
- package/dist/test/unit/webhook-secret.test.js +0 -43
- package/dist/test/unit/webhook-secret.test.js.map +0 -1
- package/frontend-vue/app/components/AttentionCard.vue +0 -78
- package/frontend-vue/app/components/HtmlPreviewModal.vue +0 -20
- package/frontend-vue/app/components/ProviderChip.vue +0 -39
- package/frontend-vue/app/components/editor/EditorBlockSettings.vue +0 -186
- package/frontend-vue/app/components/editor/EditorDataPanel.vue +0 -130
- package/frontend-vue/app/components/editor/EditorDraftBar.vue +0 -35
- package/frontend-vue/app/components/editor/EditorRail.vue +0 -58
- package/frontend-vue/app/components/editor/EditorSubjectSettings.vue +0 -37
- package/frontend-vue/app/components/editor/EditorTemplateSettings.vue +0 -87
- package/frontend-vue/app/components/editor/EditorTestSendModal.vue +0 -66
- package/frontend-vue/app/components/editor/EditorVariables.vue +0 -163
- package/frontend-vue/app/composables/useMailingPeriodData.ts +0 -39
- package/frontend-vue/tests/smoke.test.ts +0 -8
- package/src/antelope-modules.ts +0 -48
- package/src/test/antelope.test.ts +0 -190
- package/src/test/helpers/http.ts +0 -23
- package/src/test/helpers/owner.ts +0 -37
- package/src/test/integration/events.test.ts +0 -159
- package/src/test/integration/retention.test.ts +0 -363
- package/src/test/integration/sends.test.ts +0 -233
- package/src/test/integration/settings.test.ts +0 -128
- package/src/test/integration/setup.test.ts +0 -35
- package/src/test/integration/templates.test.ts +0 -272
- package/src/test/integration/tenant-export.test.ts +0 -125
- package/src/test/unit/attention.test.ts +0 -129
- package/src/test/unit/automation.test.ts +0 -130
- package/src/test/unit/categories.test.ts +0 -51
- package/src/test/unit/engine/conditions.test.ts +0 -35
- package/src/test/unit/engine/coverage.test.ts +0 -118
- package/src/test/unit/engine/fallback-html.test.ts +0 -86
- package/src/test/unit/engine/paths.test.ts +0 -39
- package/src/test/unit/engine/resolve.test.ts +0 -176
- package/src/test/unit/engine/tokens.test.ts +0 -26
- package/src/test/unit/events.test.ts +0 -19
- package/src/test/unit/metrics.test.ts +0 -170
- package/src/test/unit/notify.test.ts +0 -71
- package/src/test/unit/period.test.ts +0 -91
- package/src/test/unit/permissions.test.ts +0 -30
- package/src/test/unit/provider.test.ts +0 -57
- package/src/test/unit/realtime.test.ts +0 -11
- package/src/test/unit/render.test.ts +0 -23
- package/src/test/unit/retention.test.ts +0 -91
- package/src/test/unit/send-batch.test.ts +0 -72
- package/src/test/unit/send.test.ts +0 -161
- package/src/test/unit/smoke.test.ts +0 -8
- package/src/test/unit/status.test.ts +0 -14
- package/src/test/unit/templates.test.ts +0 -52
- package/src/test/unit/tenant-export.test.ts +0 -88
- package/src/test/unit/webhook-secret.test.ts +0 -51
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import type { TemplateRow, TemplateStatus } from '../types/mailing'
|
|
2
|
+
|
|
3
|
+
export type LifecycleAction = 'unpublish' | 'archive' | 'restore'
|
|
4
|
+
|
|
5
|
+
export interface LifecycleButton {
|
|
6
|
+
action: LifecycleAction
|
|
7
|
+
icon: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const BUTTONS: Record<LifecycleAction, LifecycleButton> = {
|
|
11
|
+
unpublish: { action: 'unpublish', icon: 'i-ph-eye-slash' },
|
|
12
|
+
archive: { action: 'archive', icon: 'i-ph-archive' },
|
|
13
|
+
restore: { action: 'restore', icon: 'i-ph-arrow-u-up-left' },
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const TRANSITIONS: Record<TemplateStatus, LifecycleAction[]> = {
|
|
17
|
+
draft: ['archive'],
|
|
18
|
+
live: ['unpublish', 'archive'],
|
|
19
|
+
archived: ['restore'],
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function lifecycleActions(status: TemplateStatus): LifecycleButton[] {
|
|
23
|
+
return (TRANSITIONS[status] ?? []).map((action) => BUTTONS[action])
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** What an action stamps on the template besides its status, at `now`. */
|
|
27
|
+
const STAMPS: Partial<
|
|
28
|
+
Record<LifecycleAction, (now: Date) => Partial<TemplateRow>>
|
|
29
|
+
> = {
|
|
30
|
+
unpublish: (now) => ({ unpublishedAt: now.toISOString() }),
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The template once `action` went through: its new status and the moments
|
|
35
|
+
* the editor's notice dates, as the server recorded them.
|
|
36
|
+
*/
|
|
37
|
+
export function afterLifecycle(
|
|
38
|
+
template: TemplateRow,
|
|
39
|
+
action: LifecycleAction,
|
|
40
|
+
status: TemplateStatus,
|
|
41
|
+
now = new Date(),
|
|
42
|
+
): TemplateRow {
|
|
43
|
+
return { ...template, ...STAMPS[action]?.(now), status }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Publishing is refused from `archived`; restore the template first. */
|
|
47
|
+
export function canPublish(status: TemplateStatus): boolean {
|
|
48
|
+
return status !== 'archived'
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Where the draft's autosave stands. */
|
|
52
|
+
export type SaveStatus = 'idle' | 'saving' | 'error' | 'conflict'
|
|
53
|
+
|
|
54
|
+
/** What decides whether "Publish changes" (and ⌘↵) can run. */
|
|
55
|
+
export interface PublishableInput {
|
|
56
|
+
status: TemplateStatus
|
|
57
|
+
changeCount: number
|
|
58
|
+
dirty: number
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Whether publishing would do anything: a live template needs a change; a
|
|
63
|
+
* draft or unpublished one goes live; an archived one must be restored first.
|
|
64
|
+
*/
|
|
65
|
+
export function isPublishable(input: PublishableInput): boolean {
|
|
66
|
+
if (!canPublish(input.status)) return false
|
|
67
|
+
if (input.status !== 'live') return true
|
|
68
|
+
return input.changeCount > 0 || input.dirty > 0
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The status a template reads as: a draft that was once live is unpublished. */
|
|
72
|
+
export type StatusKey = TemplateStatus | 'unpublished'
|
|
73
|
+
|
|
74
|
+
export function statusKeyOf(
|
|
75
|
+
status: TemplateStatus,
|
|
76
|
+
version: number,
|
|
77
|
+
): StatusKey {
|
|
78
|
+
return status === 'draft' && version > 0 ? 'unpublished' : status
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export type NoticeKind =
|
|
82
|
+
| 'conflict'
|
|
83
|
+
| 'save_error'
|
|
84
|
+
| 'unpublished'
|
|
85
|
+
| 'live_changes'
|
|
86
|
+
| 'live_clean'
|
|
87
|
+
| 'never_published'
|
|
88
|
+
|
|
89
|
+
export type NoticeAction =
|
|
90
|
+
'compare' | 'discard' | 'reload' | 'overwrite' | 'retry' | 'republish'
|
|
91
|
+
|
|
92
|
+
export type NoticeTone = 'primary' | 'warning' | 'error'
|
|
93
|
+
|
|
94
|
+
/** The band under the toolbar: what it says, its tone and its actions. */
|
|
95
|
+
export interface EditorNotice {
|
|
96
|
+
kind: NoticeKind
|
|
97
|
+
tone: NoticeTone
|
|
98
|
+
actions: NoticeAction[]
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface NoticeInput {
|
|
102
|
+
status: TemplateStatus
|
|
103
|
+
version: number
|
|
104
|
+
changeCount: number
|
|
105
|
+
saveStatus: SaveStatus
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const DRAFT_ACTIONS: NoticeAction[] = ['compare', 'discard']
|
|
109
|
+
|
|
110
|
+
const SAVE_NOTICES: Partial<Record<SaveStatus, EditorNotice>> = {
|
|
111
|
+
conflict: {
|
|
112
|
+
kind: 'conflict',
|
|
113
|
+
tone: 'error',
|
|
114
|
+
actions: ['reload', 'overwrite'],
|
|
115
|
+
},
|
|
116
|
+
error: { kind: 'save_error', tone: 'error', actions: ['retry'] },
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function contentNotice(input: NoticeInput): EditorNotice | null {
|
|
120
|
+
const draftActions = input.changeCount > 0 ? DRAFT_ACTIONS : []
|
|
121
|
+
if (input.version === 0)
|
|
122
|
+
return input.changeCount > 0
|
|
123
|
+
? { kind: 'never_published', tone: 'primary', actions: [] }
|
|
124
|
+
: null
|
|
125
|
+
if (input.status === 'live')
|
|
126
|
+
return input.changeCount > 0
|
|
127
|
+
? { kind: 'live_changes', tone: 'primary', actions: draftActions }
|
|
128
|
+
: { kind: 'live_clean', tone: 'primary', actions: [] }
|
|
129
|
+
if (input.status === 'draft')
|
|
130
|
+
return {
|
|
131
|
+
kind: 'unpublished',
|
|
132
|
+
tone: 'warning',
|
|
133
|
+
actions: [...draftActions, 'republish'],
|
|
134
|
+
}
|
|
135
|
+
return null
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Which notice the editor shows: a draft that could not be saved first, then
|
|
140
|
+
* the template's reading — nothing published yet, live, or unpublished.
|
|
141
|
+
*/
|
|
142
|
+
export function noticeFor(input: NoticeInput): EditorNotice | null {
|
|
143
|
+
return SAVE_NOTICES[input.saveStatus] ?? contentNotice(input)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** The moment a notice dates: the unpublish, else the last publication. */
|
|
147
|
+
export function noticeMomentOf(
|
|
148
|
+
kind: NoticeKind,
|
|
149
|
+
template: Pick<TemplateRow, 'publishedAt' | 'unpublishedAt' | 'updatedAt'>,
|
|
150
|
+
): string | null | undefined {
|
|
151
|
+
if (kind !== 'unpublished') return template.publishedAt
|
|
152
|
+
return template.unpublishedAt ?? template.updatedAt
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const DATE_FORMAT: Intl.DateTimeFormatOptions = {
|
|
156
|
+
month: 'short',
|
|
157
|
+
day: 'numeric',
|
|
158
|
+
}
|
|
159
|
+
const TIME_FORMAT: Intl.DateTimeFormatOptions = {
|
|
160
|
+
hour: '2-digit',
|
|
161
|
+
minute: '2-digit',
|
|
162
|
+
hour12: false,
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** A publication moment, split for "published Sep 29 at 09:14". */
|
|
166
|
+
export interface PublishedMoment {
|
|
167
|
+
date: string
|
|
168
|
+
time: string
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function publishedMoment(
|
|
172
|
+
iso: string | null | undefined,
|
|
173
|
+
locale: string,
|
|
174
|
+
): PublishedMoment | null {
|
|
175
|
+
if (!iso) return null
|
|
176
|
+
const date = new Date(iso)
|
|
177
|
+
if (Number.isNaN(date.getTime())) return null
|
|
178
|
+
return {
|
|
179
|
+
date: date.toLocaleDateString(locale, DATE_FORMAT),
|
|
180
|
+
time: date.toLocaleTimeString(locale, TIME_FORMAT),
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** The subject length mail clients show in full; past it, a warning tint. */
|
|
185
|
+
export const SUBJECT_RECOMMENDED_LENGTH = 60
|
|
@@ -5,8 +5,19 @@ export interface LocalePresence {
|
|
|
5
5
|
|
|
6
6
|
const LOCALE_LIST_SEPARATOR = ','
|
|
7
7
|
|
|
8
|
+
/** Base look of a locale chip, on a card, in a table cell or a review box. */
|
|
9
|
+
export const LOCALE_CHIP_CLASS =
|
|
10
|
+
'inline-flex h-[18px] items-center rounded px-1.5 font-mono text-[10.5px] font-semibold uppercase leading-none'
|
|
11
|
+
|
|
12
|
+
/** A locale the template carries. */
|
|
13
|
+
export const LOCALE_CHIP_PRESENT_CLASS = 'bg-elevated text-toned'
|
|
14
|
+
|
|
15
|
+
/** A workspace locale the template lacks: its recipients get the fallback. */
|
|
16
|
+
export const LOCALE_CHIP_MISSING_CLASS =
|
|
17
|
+
'text-warning border-warning/60 border border-dashed'
|
|
18
|
+
|
|
8
19
|
/** Reads the `locales` column, whose format is a comma-separated code list. */
|
|
9
|
-
export function parseLocaleList(value: string | undefined): string[] {
|
|
20
|
+
export function parseLocaleList(value: string | undefined | null): string[] {
|
|
10
21
|
return (value ?? '')
|
|
11
22
|
.split(LOCALE_LIST_SEPARATOR)
|
|
12
23
|
.map((code) => code.trim())
|
|
@@ -32,10 +43,104 @@ export function localeCoverage(
|
|
|
32
43
|
* locale is missing.
|
|
33
44
|
*/
|
|
34
45
|
export function localeBadges(
|
|
35
|
-
stored: string | undefined,
|
|
46
|
+
stored: string | undefined | null,
|
|
47
|
+
codes: string[],
|
|
48
|
+
): LocalePresence[] {
|
|
49
|
+
const present = parseLocaleList(stored)
|
|
50
|
+
if (!present.length) return []
|
|
51
|
+
return localeCoverage(present, codes)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Chips for a list of codes: the workspace ones in the workspace order, then
|
|
56
|
+
* any the template carries that the workspace no longer offers.
|
|
57
|
+
*/
|
|
58
|
+
export function localeChips(
|
|
59
|
+
stored: string | undefined | null,
|
|
36
60
|
codes: string[],
|
|
37
61
|
): LocalePresence[] {
|
|
38
62
|
const present = parseLocaleList(stored)
|
|
39
63
|
if (!present.length) return []
|
|
64
|
+
const extra = present.filter((code) => !codes.includes(code))
|
|
65
|
+
return [
|
|
66
|
+
...localeCoverage(present, codes),
|
|
67
|
+
...extra.map((code) => ({ code, present: true })),
|
|
68
|
+
]
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The workspace locales `present` lacks. */
|
|
72
|
+
export function missingLocales(present: string[], codes: string[]): string[] {
|
|
40
73
|
return localeCoverage(present, codes)
|
|
74
|
+
.filter((entry) => !entry.present)
|
|
75
|
+
.map((entry) => entry.code)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function localeChipClass(present: boolean): string {
|
|
79
|
+
return `${LOCALE_CHIP_CLASS} ${present ? LOCALE_CHIP_PRESENT_CLASS : LOCALE_CHIP_MISSING_CLASS}`
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const DEFAULT_FALLBACK_LOCALE = 'en'
|
|
83
|
+
const LOCALE_PART_SEPARATOR = /[-_]/
|
|
84
|
+
|
|
85
|
+
/** The base language of a locale code: "fr-BE" and "FR" both read "fr". */
|
|
86
|
+
export function localeLanguage(code: string): string {
|
|
87
|
+
const [language = code] = code.split(LOCALE_PART_SEPARATOR)
|
|
88
|
+
return language.toLowerCase()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The locale a send renders, as the server picks it: the requested one
|
|
93
|
+
* (regardless of case, or by its base language), else the tenant fallback,
|
|
94
|
+
* else English, else the first locale the content carries.
|
|
95
|
+
*/
|
|
96
|
+
export function pickLocale(
|
|
97
|
+
available: string[],
|
|
98
|
+
wanted: string | undefined,
|
|
99
|
+
fallback: string,
|
|
100
|
+
): string {
|
|
101
|
+
const byCode = new Map(available.map((code) => [code.toLowerCase(), code]))
|
|
102
|
+
const requested = wanted ? [wanted, localeLanguage(wanted)] : []
|
|
103
|
+
const candidates = [
|
|
104
|
+
...requested,
|
|
105
|
+
fallback,
|
|
106
|
+
DEFAULT_FALLBACK_LOCALE,
|
|
107
|
+
available[0],
|
|
108
|
+
]
|
|
109
|
+
const match = candidates
|
|
110
|
+
.map((code) => (code ? byCode.get(code.toLowerCase()) : undefined))
|
|
111
|
+
.find((code): code is string => Boolean(code))
|
|
112
|
+
return match ?? DEFAULT_FALLBACK_LOCALE
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const LANGUAGE_NAMES: Intl.DisplayNamesOptions = { type: 'language' }
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* A language named in the interface's language, capitalised like a label
|
|
119
|
+
* ("Anglais" for `en` in French); absent when the runtime does not know it.
|
|
120
|
+
*/
|
|
121
|
+
export function languageName(
|
|
122
|
+
code: string,
|
|
123
|
+
uiLocale: string,
|
|
124
|
+
): string | undefined {
|
|
125
|
+
try {
|
|
126
|
+
const name = new Intl.DisplayNames([uiLocale], LANGUAGE_NAMES).of(code)
|
|
127
|
+
if (!name || name.toLowerCase() === code.toLowerCase()) return undefined
|
|
128
|
+
return name.charAt(0).toLocaleUpperCase(uiLocale) + name.slice(1)
|
|
129
|
+
} catch {
|
|
130
|
+
return undefined
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* A locale as a picker lists it, in the interface's language: "French (FR)".
|
|
136
|
+
* `fallbackName` stands in for a language the runtime cannot name.
|
|
137
|
+
*/
|
|
138
|
+
export function languageLabel(
|
|
139
|
+
code: string,
|
|
140
|
+
uiLocale: string,
|
|
141
|
+
fallbackName?: string,
|
|
142
|
+
): string {
|
|
143
|
+
const upper = code.toUpperCase()
|
|
144
|
+
const name = languageName(code, uiLocale) ?? fallbackName
|
|
145
|
+
return name ? `${name} (${upper})` : upper
|
|
41
146
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { SETTINGS_PATH, TEMPLATES_PATH } from './pages'
|
|
2
|
+
|
|
3
|
+
export type SetupStepId = 'provider' | 'sender' | 'publish' | 'test'
|
|
4
|
+
|
|
5
|
+
export type SetupStepState = 'done' | 'current' | 'todo'
|
|
6
|
+
|
|
7
|
+
/** What `GET /metrics/setup` answers. */
|
|
8
|
+
export interface SetupFacts {
|
|
9
|
+
isProviderConnected: boolean
|
|
10
|
+
isSenderSet: boolean
|
|
11
|
+
liveTemplateCount: number
|
|
12
|
+
hasTestSend: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface SetupStepDefinition {
|
|
16
|
+
id: SetupStepId
|
|
17
|
+
/**
|
|
18
|
+
* Where the step is done; absent when it is not done in the app (the
|
|
19
|
+
* provider is set in the project's configuration).
|
|
20
|
+
*/
|
|
21
|
+
to?: string
|
|
22
|
+
isDone: (facts: SetupFacts) => boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface SetupStep {
|
|
26
|
+
id: SetupStepId
|
|
27
|
+
number: number
|
|
28
|
+
to?: string
|
|
29
|
+
state: SetupStepState
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** A settings form section, by the id the DMS form gives its anchor. */
|
|
33
|
+
const SETTINGS_SECTION = `${SETTINGS_PATH}#dms-form-content-section-`
|
|
34
|
+
|
|
35
|
+
export const SETUP_STEPS: SetupStepDefinition[] = [
|
|
36
|
+
{
|
|
37
|
+
id: 'provider',
|
|
38
|
+
isDone: (facts) => facts.isProviderConnected,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: 'sender',
|
|
42
|
+
to: `${SETTINGS_SECTION}sender`,
|
|
43
|
+
isDone: (facts) => facts.isSenderSet,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: 'publish',
|
|
47
|
+
to: TEMPLATES_PATH,
|
|
48
|
+
isDone: (facts) => facts.liveTemplateCount > 0,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: 'test',
|
|
52
|
+
to: TEMPLATES_PATH,
|
|
53
|
+
isDone: (facts) => facts.hasTestSend,
|
|
54
|
+
},
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The first-run checklist: each step done or not from what the workspace
|
|
59
|
+
* holds, the first one left marked current.
|
|
60
|
+
*/
|
|
61
|
+
export function setupSteps(facts: SetupFacts): SetupStep[] {
|
|
62
|
+
const done = SETUP_STEPS.map((step) => step.isDone(facts))
|
|
63
|
+
const current = done.indexOf(false)
|
|
64
|
+
return SETUP_STEPS.map((step, index) => ({
|
|
65
|
+
id: step.id,
|
|
66
|
+
number: index + 1,
|
|
67
|
+
to: step.to,
|
|
68
|
+
state: done[index] ? 'done' : index === current ? 'current' : 'todo',
|
|
69
|
+
}))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The dashboard only makes sense once e-mail can leave and a template is live:
|
|
74
|
+
* a draft alone sends nothing.
|
|
75
|
+
*/
|
|
76
|
+
export function needsSetup(facts: SetupFacts): boolean {
|
|
77
|
+
return !facts.isProviderConnected || facts.liveTemplateCount === 0
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Every step of the checklist is done: nothing left to resume. */
|
|
81
|
+
export function isSetupComplete(facts: SetupFacts): boolean {
|
|
82
|
+
return SETUP_STEPS.every((step) => step.isDone(facts))
|
|
83
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** The module's pages, as `navigateDms` and links reach them. */
|
|
2
|
+
export const TEMPLATES_PATH = '/modules/mailing/templates'
|
|
3
|
+
export const SENDS_PATH = '/modules/mailing/sends'
|
|
4
|
+
export const SETTINGS_PATH = '/modules/mailing/settings'
|
|
5
|
+
export const SUPPRESSIONS_PATH = '/modules/mailing/suppressions'
|
|
@@ -2,15 +2,37 @@ import type { Block, LocaleContent, TemplateContent } from '../types/mailing'
|
|
|
2
2
|
import { splitTokens } from './tokens'
|
|
3
3
|
|
|
4
4
|
const TOKEN_FIELDS = ['text', 'value', 'href', 'label', 'alt', 'imageUrl']
|
|
5
|
+
const PATH_SEPARATOR = '.'
|
|
6
|
+
const ARRAY_SUFFIX = '[]'
|
|
7
|
+
const RESERVED_SEGMENTS = ['__proto__', 'constructor', 'prototype']
|
|
8
|
+
|
|
9
|
+
export const UNSUBSCRIBE_URL_PATH = 'unsubscribeUrl'
|
|
10
|
+
export const PREFERENCES_URL_PATH = 'preferencesUrl'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The footer links' URLs, passed by the caller at send time. The content
|
|
14
|
+
* declares them itself (`utils/system-variables.ts`), so they never show as
|
|
15
|
+
* undeclared; the backend keeps them out of `detectedVariables` the same way.
|
|
16
|
+
*/
|
|
17
|
+
export const SYSTEM_PATHS = [UNSUBSCRIBE_URL_PATH, PREFERENCES_URL_PATH]
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A path cut after each separator ("order." + "number"): where a narrow
|
|
21
|
+
* column may wrap it, rather than in the middle of a name.
|
|
22
|
+
*/
|
|
23
|
+
export function pathBreakSegments(path: string): string[] {
|
|
24
|
+
return path
|
|
25
|
+
.split(PATH_SEPARATOR)
|
|
26
|
+
.map((segment, index, segments) =>
|
|
27
|
+
index < segments.length - 1 ? `${segment}${PATH_SEPARATOR}` : segment,
|
|
28
|
+
)
|
|
29
|
+
}
|
|
5
30
|
|
|
6
31
|
function tokenPaths(block: Block): string[] {
|
|
7
32
|
const record = block as unknown as Record<string, unknown>
|
|
8
33
|
return TOKEN_FIELDS.flatMap((key) => {
|
|
9
34
|
const value = record[key]
|
|
10
|
-
|
|
11
|
-
return splitTokens(value)
|
|
12
|
-
.filter((part) => part.kind === 'token')
|
|
13
|
-
.map((part) => part.value)
|
|
35
|
+
return typeof value === 'string' ? textPaths(value) : []
|
|
14
36
|
})
|
|
15
37
|
}
|
|
16
38
|
|
|
@@ -46,13 +68,8 @@ export function collectPaths(
|
|
|
46
68
|
return [...new Set(all.filter(Boolean))].sort()
|
|
47
69
|
}
|
|
48
70
|
|
|
49
|
-
/**
|
|
50
|
-
|
|
51
|
-
* backend drops these before answering; this list mirrors it.
|
|
52
|
-
*/
|
|
53
|
-
const SYSTEM_PATHS = ['unsubscribeUrl', 'preferencesUrl']
|
|
54
|
-
|
|
55
|
-
function textPaths(text: string): string[] {
|
|
71
|
+
/** The variable paths a text references through `{{path}}` tokens. */
|
|
72
|
+
export function textPaths(text: string): string[] {
|
|
56
73
|
return splitTokens(text)
|
|
57
74
|
.filter((part) => part.kind === 'token')
|
|
58
75
|
.map((part) => part.value)
|
|
@@ -69,10 +86,6 @@ function pathsInLocale(locale: LocaleContent): string[] {
|
|
|
69
86
|
/**
|
|
70
87
|
* Every variable path the content references, across all of its locales — the
|
|
71
88
|
* live equivalent of what the backend returns as `detectedVariables`.
|
|
72
|
-
*
|
|
73
|
-
* The panel needs this because the server's list only refreshes on save: until
|
|
74
|
-
* then a path wired into a display condition reads as unused, which is exactly
|
|
75
|
-
* the moment an author looks at it.
|
|
76
89
|
*/
|
|
77
90
|
export function collectContentPaths(
|
|
78
91
|
content: Pick<TemplateContent, 'locales'>,
|
|
@@ -82,3 +95,26 @@ export function collectContentPaths(
|
|
|
82
95
|
...new Set(paths.filter((path) => path && !SYSTEM_PATHS.includes(path))),
|
|
83
96
|
].sort()
|
|
84
97
|
}
|
|
98
|
+
|
|
99
|
+
function readSegment(current: unknown, segment: string): unknown {
|
|
100
|
+
if (current === null || typeof current !== 'object') return undefined
|
|
101
|
+
if (RESERVED_SEGMENTS.includes(segment)) return undefined
|
|
102
|
+
return (current as Record<string, unknown>)[segment]
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Reads `path` (`order.total`, `order.lines[]`) in `data`, the way the backend
|
|
107
|
+
* engine does; `undefined` when any segment is missing.
|
|
108
|
+
*/
|
|
109
|
+
export function readPath(data: unknown, path: string): unknown {
|
|
110
|
+
const trimmed = path.trim()
|
|
111
|
+
const normalized = trimmed.endsWith(ARRAY_SUFFIX)
|
|
112
|
+
? trimmed.slice(0, -ARRAY_SUFFIX.length)
|
|
113
|
+
: trimmed
|
|
114
|
+
return normalized.split(PATH_SEPARATOR).reduce<unknown>(readSegment, data)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Whether `value` counts as provided: neither `undefined` nor `null`. */
|
|
118
|
+
export function isPresent(value: unknown): boolean {
|
|
119
|
+
return value !== undefined && value !== null
|
|
120
|
+
}
|
|
@@ -1,23 +1,63 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
*/
|
|
7
|
-
export
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
1
|
+
const MINUTE_MS = 60_000
|
|
2
|
+
const DAY_MS = 86_400_000
|
|
3
|
+
const PRESET_KEY_SEPARATOR = /-/g
|
|
4
|
+
const KEY_SEPARATOR = '_'
|
|
5
|
+
|
|
6
|
+
/** Hour and minute on a 24-hour clock, the way the cards stamp "updated 14:32". */
|
|
7
|
+
export const CLOCK_FORMAT: Intl.DateTimeFormatOptions = {
|
|
8
|
+
hour: '2-digit',
|
|
9
|
+
minute: '2-digit',
|
|
10
|
+
hourCycle: 'h23',
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** The i18n key of a preset's short segmented label ("24H", "7D"…). */
|
|
14
|
+
export function presetShortLabelKey(preset: string): string {
|
|
15
|
+
return `dms_mailing.period.short.${preset.replace(PRESET_KEY_SEPARATOR, KEY_SEPARATOR)}`
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Whole minutes elapsed since `iso`; 0 for a missing or future date. */
|
|
19
|
+
export function minutesSince(
|
|
20
|
+
iso: string | null | undefined,
|
|
21
|
+
now: Date,
|
|
22
|
+
): number {
|
|
23
|
+
if (!iso) return 0
|
|
24
|
+
const elapsed = now.getTime() - new Date(iso).getTime()
|
|
25
|
+
return elapsed > 0 ? Math.floor(elapsed / MINUTE_MS) : 0
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Formats a date as HH:MM in `locale`. */
|
|
29
|
+
export function formatClock(value: Date | string, locale: string): string {
|
|
30
|
+
return new Date(value).toLocaleTimeString(locale, CLOCK_FORMAT)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Whether `at` falls within the next 24 hours: "tonight" rather than a date. */
|
|
34
|
+
export function isWithinADay(at: Date | string, now: Date): boolean {
|
|
35
|
+
const delay = new Date(at).getTime() - now.getTime()
|
|
36
|
+
return delay >= 0 && delay < DAY_MS
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Retention presets of the settings form, in days. */
|
|
40
|
+
export const RETENTION_PRESETS = [30, 90, 365]
|
|
41
|
+
export const RETENTION_CUSTOM = 'custom'
|
|
42
|
+
export const MIN_RETENTION_DAYS = 1
|
|
43
|
+
export const MAX_RETENTION_DAYS = 3650
|
|
44
|
+
|
|
45
|
+
export type RetentionChoice = string
|
|
46
|
+
|
|
47
|
+
/** The segment a retention shows: its preset, else "custom". */
|
|
48
|
+
export function retentionChoice(
|
|
49
|
+
days: number | null | undefined,
|
|
50
|
+
): RetentionChoice {
|
|
51
|
+
return days && RETENTION_PRESETS.includes(days)
|
|
52
|
+
? String(days)
|
|
53
|
+
: RETENTION_CUSTOM
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** A typed retention brought back between 1 day and 10 years, whole days. */
|
|
57
|
+
export function clampRetention(days: number): number {
|
|
58
|
+
if (!Number.isFinite(days)) return MIN_RETENTION_DAYS
|
|
59
|
+
return Math.min(
|
|
60
|
+
MAX_RETENTION_DAYS,
|
|
61
|
+
Math.max(MIN_RETENTION_DAYS, Math.round(days)),
|
|
62
|
+
)
|
|
23
63
|
}
|