@antelopejs/dms-mailing 0.6.0 → 0.7.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/dist/constants.d.ts +1 -0
- package/dist/constants.js +2 -1
- package/dist/constants.js.map +1 -1
- package/dist/data/templates-table.d.ts +4 -1
- package/dist/data/templates-table.js +55 -13
- package/dist/data/templates-table.js.map +1 -1
- package/dist/data-types/categories-type.d.ts +5 -5
- package/dist/db/models/templates.model.d.ts +10 -1
- package/dist/db/models/templates.model.js +27 -1
- package/dist/db/models/templates.model.js.map +1 -1
- package/dist/db/tables/templates.table.d.ts +7 -1
- package/dist/db/tables/templates.table.js +3 -1
- package/dist/db/tables/templates.table.js.map +1 -1
- package/dist/engine/formats.d.ts +13 -0
- package/dist/engine/formats.js +85 -0
- package/dist/engine/formats.js.map +1 -0
- package/dist/engine/index.d.ts +1 -0
- package/dist/engine/index.js +1 -0
- package/dist/engine/index.js.map +1 -1
- package/dist/engine/resolve.d.ts +2 -1
- package/dist/engine/resolve.js +14 -7
- package/dist/engine/resolve.js.map +1 -1
- package/dist/engine/tokens.d.ts +2 -1
- package/dist/engine/tokens.js +3 -5
- package/dist/engine/tokens.js.map +1 -1
- package/dist/frontend-module.d.ts +2 -0
- package/dist/frontend-module.js +24 -0
- package/dist/frontend-module.js.map +1 -0
- package/dist/hooks/tenant-export.js +4 -1
- package/dist/hooks/tenant-export.js.map +1 -1
- package/dist/implementations/dms-mailing/index.d.ts +1 -0
- package/dist/implementations/dms-mailing/index.js +3 -1
- package/dist/implementations/dms-mailing/index.js.map +1 -1
- package/dist/index.js +2 -19
- package/dist/index.js.map +1 -1
- package/dist/pages/overview.d.ts +1 -1
- package/dist/pages/overview.js +11 -13
- package/dist/pages/overview.js.map +1 -1
- package/dist/pages/sends.d.ts +1 -1
- package/dist/pages/sends.js +16 -8
- package/dist/pages/sends.js.map +1 -1
- package/dist/pages/templates.js +3 -8
- package/dist/pages/templates.js.map +1 -1
- package/dist/pages/toolbar.d.ts +12 -0
- package/dist/pages/toolbar.js +41 -0
- package/dist/pages/toolbar.js.map +1 -0
- package/dist/realtime.d.ts +2 -2
- package/dist/realtime.js +8 -8
- package/dist/realtime.js.map +1 -1
- package/dist/routes/metrics.d.ts +9 -2
- package/dist/routes/metrics.js +90 -35
- package/dist/routes/metrics.js.map +1 -1
- package/dist/routes/period.d.ts +1 -0
- package/dist/routes/period.js +19 -0
- package/dist/routes/period.js.map +1 -1
- package/dist/routes/sends.js +6 -3
- package/dist/routes/sends.js.map +1 -1
- package/dist/routes/settings.js +6 -7
- package/dist/routes/settings.js.map +1 -1
- package/dist/routes/templates.d.ts +8 -4
- package/dist/routes/templates.js +40 -15
- package/dist/routes/templates.js.map +1 -1
- package/dist/services/categories.d.ts +2 -1
- package/dist/services/categories.js +9 -0
- package/dist/services/categories.js.map +1 -1
- package/dist/services/declarations.js +2 -2
- package/dist/services/declarations.js.map +1 -1
- package/dist/services/events.js +1 -1
- package/dist/services/events.js.map +1 -1
- package/dist/services/localized-text.d.ts +7 -0
- package/dist/services/localized-text.js +28 -0
- package/dist/services/localized-text.js.map +1 -0
- package/dist/services/send-stat-items.js +34 -12
- package/dist/services/send-stat-items.js.map +1 -1
- package/dist/services/send-stats.d.ts +7 -0
- package/dist/services/send-stats.js +17 -5
- package/dist/services/send-stats.js.map +1 -1
- package/dist/services/send.d.ts +2 -0
- package/dist/services/send.js +11 -12
- package/dist/services/send.js.map +1 -1
- package/dist/services/sender-declaration.d.ts +2 -0
- package/dist/services/sender-declaration.js +18 -0
- package/dist/services/sender-declaration.js.map +1 -0
- package/dist/services/starters.d.ts +2 -2
- package/dist/services/starters.js +1 -10
- package/dist/services/starters.js.map +1 -1
- package/dist/services/templates.d.ts +11 -5
- package/dist/services/templates.js +24 -6
- package/dist/services/templates.js.map +1 -1
- package/dist/services/webhook-status.d.ts +2 -1
- package/dist/services/webhook-status.js +6 -5
- package/dist/services/webhook-status.js.map +1 -1
- package/dist/types/settings.d.ts +6 -0
- package/dist/types/settings.js +25 -5
- package/dist/types/settings.js.map +1 -1
- package/dist/validation/declarations.schema.d.ts +25 -7
- package/dist/validation/declarations.schema.js +16 -3
- package/dist/validation/declarations.schema.js.map +1 -1
- package/dist/validation/settings.schema.d.ts +25 -25
- package/dist/validation/settings.schema.js +30 -6
- package/dist/validation/settings.schema.js.map +1 -1
- package/dist/validation/templates.schema.d.ts +13 -0
- package/dist/validation/templates.schema.js +6 -1
- package/dist/validation/templates.schema.js.map +1 -1
- package/frontend-vue/app/components/CategoriesInput.vue +15 -13
- package/frontend-vue/app/components/FunnelCard.vue +46 -3
- package/frontend-vue/app/components/NewTemplateModal.vue +4 -2
- package/frontend-vue/app/components/OverviewGate.vue +0 -8
- package/frontend-vue/app/components/ProviderPill.vue +50 -0
- package/frontend-vue/app/components/SendDrawer.vue +206 -183
- package/frontend-vue/app/components/StarterGallery.vue +1 -1
- package/frontend-vue/app/components/TemplateCard.vue +4 -1
- package/frontend-vue/app/components/TemplateDrawer.vue +3 -5
- package/frontend-vue/app/components/TemplateGallery.vue +57 -17
- package/frontend-vue/app/components/TemplatePreviewFrame.vue +41 -11
- package/frontend-vue/app/components/editor/EditorTemplatePane.vue +1 -3
- package/frontend-vue/app/components/editor/EditorTitle.vue +1 -7
- package/frontend-vue/app/composables/useLocalizedDefaults.ts +5 -7
- package/frontend-vue/app/composables/useMailingApi.ts +3 -3
- package/frontend-vue/app/composables/useMailingReads.ts +2 -2
- package/frontend-vue/app/composables/useTemplateCategories.ts +8 -7
- package/frontend-vue/app/types/mailing.ts +18 -0
- package/frontend-vue/app/utils/categories.ts +58 -49
- package/frontend-vue/app/utils/funnel.ts +25 -0
- package/frontend-vue/app/utils/period.ts +0 -7
- package/frontend-vue/app/utils/starters.ts +3 -1
- package/frontend-vue/app/utils/tokens.ts +6 -1
- package/frontend-vue/i18n/locales/mailing-en-GB.json +19 -28
- package/frontend-vue/i18n/locales/mailing-fr-FR.json +19 -28
- package/frontend-vue/package.json +1 -1
- package/frontend-vue/tests/categories-input.test.ts +4 -10
- package/frontend-vue/tests/categories.test.ts +55 -17
- package/frontend-vue/tests/conditions.test.ts +1 -4
- package/frontend-vue/tests/funnel-card.test.ts +27 -0
- package/frontend-vue/tests/funnel.test.ts +25 -0
- package/frontend-vue/tests/localized-defaults.test.ts +0 -29
- package/frontend-vue/tests/new-template-modal.test.ts +1 -1
- package/frontend-vue/tests/overview-gate.test.ts +1 -24
- package/frontend-vue/tests/page-reads.test.ts +9 -25
- package/frontend-vue/tests/period.test.ts +0 -12
- package/frontend-vue/tests/replay-toast.test.ts +2 -1
- package/frontend-vue/tests/send-drawer.test.ts +123 -0
- package/frontend-vue/tests/template-drawer.test.ts +1 -1
- package/frontend-vue/tests/template-gallery.test.ts +90 -3
- package/frontend-vue/tests/template-preview-frame.test.ts +92 -0
- package/package.json +6 -5
- package/skills/dms-mailing/SKILL.md +17 -4
- package/src/constants.ts +2 -0
- package/src/data/sends-table.ts +1 -1
- package/src/data/templates-table.ts +123 -19
- package/src/db/models/templates.model.ts +61 -3
- package/src/db/tables/templates.table.ts +10 -1
- package/src/engine/formats.ts +124 -0
- package/src/engine/index.ts +1 -0
- package/src/engine/resolve.ts +18 -7
- package/src/engine/tokens.ts +7 -5
- package/src/frontend-module.ts +36 -0
- package/src/hooks/tenant-export.ts +8 -1
- package/src/implementations/dms-mailing/index.ts +2 -0
- package/src/index.ts +2 -23
- package/src/pages/overview.ts +20 -16
- package/src/pages/sends.ts +27 -11
- package/src/pages/templates.ts +4 -8
- package/src/pages/toolbar.ts +77 -0
- package/src/realtime.ts +17 -8
- package/src/routes/metrics.ts +158 -45
- package/src/routes/period.ts +32 -0
- package/src/routes/sends.ts +13 -4
- package/src/routes/settings.ts +11 -8
- package/src/routes/templates.ts +72 -26
- package/src/services/categories.ts +14 -1
- package/src/services/declarations.ts +4 -3
- package/src/services/events.ts +1 -1
- package/src/services/localized-text.ts +53 -0
- package/src/services/send-stat-items.ts +60 -13
- package/src/services/send-stats.ts +37 -5
- package/src/services/send.ts +19 -20
- package/src/services/sender-declaration.ts +27 -0
- package/src/services/starters.ts +3 -15
- package/src/services/templates.ts +50 -19
- package/src/services/webhook-status.ts +3 -3
- package/src/types/settings.ts +35 -5
- package/src/validation/declarations.schema.ts +29 -3
- package/src/validation/settings.schema.ts +56 -14
- package/src/validation/templates.schema.ts +10 -0
- package/frontend-vue/app/components/PageHeader.vue +0 -94
- package/frontend-vue/app/composables/useOverviewSetup.ts +0 -21
|
@@ -437,11 +437,13 @@
|
|
|
437
437
|
"metrics": {
|
|
438
438
|
"bounces": "Bounces",
|
|
439
439
|
"click_rate": "Click rate",
|
|
440
|
+
"compared": "Compared period",
|
|
440
441
|
"deliverability": "Deliverability",
|
|
441
442
|
"domain_sends": "Domains with 5 or more deliveries",
|
|
442
443
|
"domains": "Open rate by domain",
|
|
443
444
|
"open_rate": "Open rate",
|
|
444
445
|
"previous": "Previous period",
|
|
446
|
+
"previous_year": "Previous year",
|
|
445
447
|
"ranking": "Best opened templates",
|
|
446
448
|
"sends": "Sends",
|
|
447
449
|
"unsubscribes": "Unsubscribes",
|
|
@@ -532,7 +534,8 @@
|
|
|
532
534
|
"send_again": "Send again…",
|
|
533
535
|
"copy_address": "Copy the address"
|
|
534
536
|
},
|
|
535
|
-
"
|
|
537
|
+
"heading": "{recipientEmail} · {templateSlug}",
|
|
538
|
+
"position": "Send {index} of {total}",
|
|
536
539
|
"replayed_by": "Sent again on {date}",
|
|
537
540
|
"replay_of": "This send repeats an earlier one",
|
|
538
541
|
"see_send": "See the send",
|
|
@@ -619,8 +622,9 @@
|
|
|
619
622
|
"latency_detail": "API call to {provider}",
|
|
620
623
|
"latency_detail_generic": "API call to the provider",
|
|
621
624
|
"joined": "{left} · {right}",
|
|
622
|
-
"
|
|
623
|
-
"
|
|
625
|
+
"versus": "vs {previous}",
|
|
626
|
+
"delta_up_versus": "▲ {delta} vs {previous}",
|
|
627
|
+
"delta_down_versus": "▼ {delta} vs {previous}",
|
|
624
628
|
"rate": "{rate}",
|
|
625
629
|
"latency_ms": "{value} ms",
|
|
626
630
|
"latency_s": "{value} s",
|
|
@@ -999,9 +1003,6 @@
|
|
|
999
1003
|
"archived": "Archived",
|
|
1000
1004
|
"attention": "Needs attention"
|
|
1001
1005
|
},
|
|
1002
|
-
"filters": {
|
|
1003
|
-
"category": "Category"
|
|
1004
|
-
},
|
|
1005
1006
|
"footer": {
|
|
1006
1007
|
"count": "{count} template | {count} templates",
|
|
1007
1008
|
"hint": "Open a template to edit it · ↵ open · T test send"
|
|
@@ -1075,8 +1076,12 @@
|
|
|
1075
1076
|
"description": "The overview dashboard, or the setup steps until the workspace can send."
|
|
1076
1077
|
},
|
|
1077
1078
|
"page_header": {
|
|
1078
|
-
"name": "
|
|
1079
|
-
"description": "The provider state and the period switch
|
|
1079
|
+
"name": "Page toolbar",
|
|
1080
|
+
"description": "The provider state, the last update, live mode and the period switch at the top of the page."
|
|
1081
|
+
},
|
|
1082
|
+
"provider_pill": {
|
|
1083
|
+
"name": "Provider state",
|
|
1084
|
+
"description": "The pill in the page toolbar saying whether a provider carries the mail."
|
|
1080
1085
|
},
|
|
1081
1086
|
"settings_form": {
|
|
1082
1087
|
"name": "Mailing settings",
|
|
@@ -1194,15 +1199,8 @@
|
|
|
1194
1199
|
"current_sender": "Sender as set in the settings today",
|
|
1195
1200
|
"meant_for": "The e-mail meant for {name}"
|
|
1196
1201
|
},
|
|
1197
|
-
"
|
|
1198
|
-
"
|
|
1199
|
-
"last_hour": "1H",
|
|
1200
|
-
"last_24h": "24H",
|
|
1201
|
-
"today": "1D",
|
|
1202
|
-
"last_7_days": "7D",
|
|
1203
|
-
"last_30_days": "30D",
|
|
1204
|
-
"last_90_days": "90D"
|
|
1205
|
-
}
|
|
1202
|
+
"live": {
|
|
1203
|
+
"new_sends": "{count} new send | {count} new sends"
|
|
1206
1204
|
},
|
|
1207
1205
|
"page_header": {
|
|
1208
1206
|
"provider_connected": "{name} connected",
|
|
@@ -1266,11 +1264,13 @@
|
|
|
1266
1264
|
"error_title": "The funnel could not be loaded",
|
|
1267
1265
|
"retry": "Try again",
|
|
1268
1266
|
"untracked": "Delivery is not measured: connect the provider’s webhook to follow the funnel.",
|
|
1269
|
-
"no_loss": "No e-mail was lost between the steps."
|
|
1267
|
+
"no_loss": "No e-mail was lost between the steps.",
|
|
1268
|
+
"compared_count": "vs {value} in the compared period"
|
|
1270
1269
|
},
|
|
1271
1270
|
"common": {
|
|
1272
1271
|
"cancel": "Cancel",
|
|
1273
|
-
"on_date": "{date}"
|
|
1272
|
+
"on_date": "{date}",
|
|
1273
|
+
"preview_unavailable": "Preview unavailable"
|
|
1274
1274
|
},
|
|
1275
1275
|
"duplicate": {
|
|
1276
1276
|
"title": "Duplicate template",
|
|
@@ -1389,15 +1389,6 @@
|
|
|
1389
1389
|
"boolean": "Yes / no",
|
|
1390
1390
|
"array": "List"
|
|
1391
1391
|
},
|
|
1392
|
-
"categories": {
|
|
1393
|
-
"defaults": {
|
|
1394
|
-
"orders": "Orders",
|
|
1395
|
-
"account": "Account & access",
|
|
1396
|
-
"billing": "Billing",
|
|
1397
|
-
"marketing": "Marketing",
|
|
1398
|
-
"forms": "Forms"
|
|
1399
|
-
}
|
|
1400
|
-
},
|
|
1401
1392
|
"starters": {
|
|
1402
1393
|
"names": {
|
|
1403
1394
|
"order-confirmed": "Order confirmation",
|
|
@@ -437,11 +437,13 @@
|
|
|
437
437
|
"metrics": {
|
|
438
438
|
"bounces": "Rebonds",
|
|
439
439
|
"click_rate": "Taux de clic",
|
|
440
|
+
"compared": "Période comparée",
|
|
440
441
|
"deliverability": "Délivrabilité",
|
|
441
442
|
"domain_sends": "Domaines avec 5 distributions ou plus",
|
|
442
443
|
"domains": "Taux d’ouverture par domaine",
|
|
443
444
|
"open_rate": "Taux d’ouverture",
|
|
444
445
|
"previous": "Période précédente",
|
|
446
|
+
"previous_year": "Année précédente",
|
|
445
447
|
"ranking": "Modèles les plus ouverts",
|
|
446
448
|
"sends": "Envois",
|
|
447
449
|
"unsubscribes": "Désinscriptions",
|
|
@@ -532,7 +534,8 @@
|
|
|
532
534
|
"send_again": "Renvoyer…",
|
|
533
535
|
"copy_address": "Copier l’adresse"
|
|
534
536
|
},
|
|
535
|
-
"
|
|
537
|
+
"heading": "{recipientEmail} · {templateSlug}",
|
|
538
|
+
"position": "Envoi {index} sur {total}",
|
|
536
539
|
"replayed_by": "Renvoyé le {date}",
|
|
537
540
|
"replay_of": "Cet envoi répète un envoi précédent",
|
|
538
541
|
"see_send": "Voir l’envoi",
|
|
@@ -619,8 +622,9 @@
|
|
|
619
622
|
"latency_detail": "Appel API à {provider}",
|
|
620
623
|
"latency_detail_generic": "Appel API au fournisseur",
|
|
621
624
|
"joined": "{left} · {right}",
|
|
622
|
-
"
|
|
623
|
-
"
|
|
625
|
+
"versus": "vs {previous}",
|
|
626
|
+
"delta_up_versus": "▲ {delta} vs {previous}",
|
|
627
|
+
"delta_down_versus": "▼ {delta} vs {previous}",
|
|
624
628
|
"rate": "{rate}",
|
|
625
629
|
"latency_ms": "{value} ms",
|
|
626
630
|
"latency_s": "{value} s",
|
|
@@ -999,9 +1003,6 @@
|
|
|
999
1003
|
"archived": "Archivés",
|
|
1000
1004
|
"attention": "À revoir"
|
|
1001
1005
|
},
|
|
1002
|
-
"filters": {
|
|
1003
|
-
"category": "Catégorie"
|
|
1004
|
-
},
|
|
1005
1006
|
"footer": {
|
|
1006
1007
|
"count": "aucun modèle | {count} modèle | {count} modèles",
|
|
1007
1008
|
"hint": "Ouvrez un modèle pour le modifier · ↵ ouvrir · T envoi de test"
|
|
@@ -1075,8 +1076,12 @@
|
|
|
1075
1076
|
"description": "Le tableau de bord, ou les étapes de configuration tant que l’espace de travail ne peut pas envoyer."
|
|
1076
1077
|
},
|
|
1077
1078
|
"page_header": {
|
|
1078
|
-
"name": "
|
|
1079
|
-
"description": "L’état du fournisseur et le choix de période
|
|
1079
|
+
"name": "Barre de page",
|
|
1080
|
+
"description": "L’état du fournisseur, la dernière mise à jour, le mode direct et le choix de la période en haut de la page."
|
|
1081
|
+
},
|
|
1082
|
+
"provider_pill": {
|
|
1083
|
+
"name": "État du fournisseur",
|
|
1084
|
+
"description": "La pastille de la barre de page qui dit si un fournisseur achemine les e-mails."
|
|
1080
1085
|
},
|
|
1081
1086
|
"settings_form": {
|
|
1082
1087
|
"name": "Réglages Mailing",
|
|
@@ -1194,15 +1199,8 @@
|
|
|
1194
1199
|
"current_sender": "Expéditeur actuel des réglages",
|
|
1195
1200
|
"meant_for": "L’e-mail destiné à {name}"
|
|
1196
1201
|
},
|
|
1197
|
-
"
|
|
1198
|
-
"
|
|
1199
|
-
"last_hour": "1 h",
|
|
1200
|
-
"last_24h": "24 h",
|
|
1201
|
-
"today": "1 j",
|
|
1202
|
-
"last_7_days": "7 j",
|
|
1203
|
-
"last_30_days": "30 j",
|
|
1204
|
-
"last_90_days": "90 j"
|
|
1205
|
-
}
|
|
1202
|
+
"live": {
|
|
1203
|
+
"new_sends": "{count} nouvel envoi | {count} nouvel envoi | {count} nouveaux envois"
|
|
1206
1204
|
},
|
|
1207
1205
|
"page_header": {
|
|
1208
1206
|
"provider_connected": "{name} connecté",
|
|
@@ -1266,11 +1264,13 @@
|
|
|
1266
1264
|
"error_title": "Impossible de charger l’entonnoir",
|
|
1267
1265
|
"retry": "Réessayer",
|
|
1268
1266
|
"untracked": "La distribution n’est pas mesurée : branchez le webhook du fournisseur pour suivre l’entonnoir.",
|
|
1269
|
-
"no_loss": "Aucun e-mail perdu entre les étapes."
|
|
1267
|
+
"no_loss": "Aucun e-mail perdu entre les étapes.",
|
|
1268
|
+
"compared_count": "vs {value} sur la période comparée"
|
|
1270
1269
|
},
|
|
1271
1270
|
"common": {
|
|
1272
1271
|
"cancel": "Annuler",
|
|
1273
|
-
"on_date": "le {date}"
|
|
1272
|
+
"on_date": "le {date}",
|
|
1273
|
+
"preview_unavailable": "Aperçu indisponible"
|
|
1274
1274
|
},
|
|
1275
1275
|
"duplicate": {
|
|
1276
1276
|
"title": "Dupliquer le modèle",
|
|
@@ -1389,15 +1389,6 @@
|
|
|
1389
1389
|
"boolean": "Oui / non",
|
|
1390
1390
|
"array": "Liste"
|
|
1391
1391
|
},
|
|
1392
|
-
"categories": {
|
|
1393
|
-
"defaults": {
|
|
1394
|
-
"orders": "Commandes",
|
|
1395
|
-
"account": "Compte et accès",
|
|
1396
|
-
"billing": "Facturation",
|
|
1397
|
-
"marketing": "Marketing",
|
|
1398
|
-
"forms": "Formulaires"
|
|
1399
|
-
}
|
|
1400
|
-
},
|
|
1401
1392
|
"starters": {
|
|
1402
1393
|
"names": {
|
|
1403
1394
|
"order-confirmed": "Confirmation de commande",
|
|
@@ -10,17 +10,11 @@ import {
|
|
|
10
10
|
type Mounted,
|
|
11
11
|
} from './helpers/nuxt-globals'
|
|
12
12
|
import { stubApiData, type ApiDataStub } from './helpers/api-data'
|
|
13
|
-
import type {
|
|
13
|
+
import type { StoredCategory } from '../app/types/mailing'
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}),
|
|
19
|
-
}))
|
|
20
|
-
|
|
21
|
-
const CATEGORIES: TemplateCategory[] = [
|
|
22
|
-
{ id: 'orders', label: 'Orders', icon: 'i-ph-package' },
|
|
23
|
-
{ id: 'marketing', label: 'Marketing', icon: 'i-ph-sparkle' },
|
|
15
|
+
const CATEGORIES: StoredCategory[] = [
|
|
16
|
+
{ id: 'orders', label: { en: 'Orders' }, icon: 'i-ph-package' },
|
|
17
|
+
{ id: 'marketing', label: { en: 'Marketing' }, icon: 'i-ph-sparkle' },
|
|
24
18
|
]
|
|
25
19
|
const ROW_ERROR = 'dms_mailing.settings.categories.label_required'
|
|
26
20
|
|
|
@@ -14,14 +14,15 @@ import {
|
|
|
14
14
|
slugifyId,
|
|
15
15
|
toCategoryOption,
|
|
16
16
|
toStoredCategories,
|
|
17
|
+
toEditableRows,
|
|
17
18
|
toStoredCategory,
|
|
18
19
|
uniqueId,
|
|
19
20
|
type EditableCategory,
|
|
20
21
|
} from '../app/utils/categories'
|
|
21
|
-
import type {
|
|
22
|
+
import type { StoredCategory } from '../app/types/mailing'
|
|
22
23
|
|
|
23
|
-
const CATEGORIES:
|
|
24
|
-
{ id: 'orders', label: 'Orders', icon: 'i-ph-package' },
|
|
24
|
+
const CATEGORIES: StoredCategory[] = [
|
|
25
|
+
{ id: 'orders', label: { en: 'Orders' }, icon: 'i-ph-package' },
|
|
25
26
|
]
|
|
26
27
|
|
|
27
28
|
describe('slugifyId', () => {
|
|
@@ -89,22 +90,56 @@ const row = (patch: Partial<EditableCategory>): EditableCategory => ({
|
|
|
89
90
|
label: '',
|
|
90
91
|
icon: 'i-ph-package',
|
|
91
92
|
isNew: false,
|
|
93
|
+
labels: {},
|
|
92
94
|
...patch,
|
|
93
95
|
})
|
|
94
96
|
|
|
95
97
|
describe('toStoredCategories', () => {
|
|
96
98
|
it('keeps saved ids even after a rename', () => {
|
|
97
99
|
expect(
|
|
98
|
-
toStoredCategories([row({ id: 'orders', label: 'Purchases' })]),
|
|
99
|
-
).toEqual([
|
|
100
|
+
toStoredCategories([row({ id: 'orders', label: 'Purchases' })], 'en'),
|
|
101
|
+
).toEqual([
|
|
102
|
+
{ id: 'orders', label: { en: 'Purchases' }, icon: 'i-ph-package' },
|
|
103
|
+
])
|
|
100
104
|
})
|
|
101
105
|
|
|
102
|
-
it('
|
|
103
|
-
const stored =
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
106
|
+
it('shows each label in the language edited and writes it back there only', () => {
|
|
107
|
+
const stored: StoredCategory[] = [
|
|
108
|
+
{
|
|
109
|
+
id: 'orders',
|
|
110
|
+
label: { en: 'Orders', fr: 'Commandes' },
|
|
111
|
+
icon: 'i-ph-package',
|
|
112
|
+
},
|
|
113
|
+
{ id: 'billing', label: { en: 'Billing' }, icon: 'i-ph-file-text' },
|
|
114
|
+
]
|
|
115
|
+
let next = 0
|
|
116
|
+
const rows = toEditableRows(stored, 'fr', () => `row-${next++}`)
|
|
117
|
+
expect(rows.map((entry) => entry.label)).toEqual(['Commandes', 'Billing'])
|
|
118
|
+
rows[0]!.label = 'Achats'
|
|
119
|
+
rows[1]!.label = 'Facturation'
|
|
120
|
+
expect(toStoredCategories(rows, 'fr')).toEqual([
|
|
121
|
+
{
|
|
122
|
+
id: 'orders',
|
|
123
|
+
label: { en: 'Orders', fr: 'Achats' },
|
|
124
|
+
icon: 'i-ph-package',
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
id: 'billing',
|
|
128
|
+
label: { en: 'Billing', fr: 'Facturation' },
|
|
129
|
+
icon: 'i-ph-file-text',
|
|
130
|
+
},
|
|
107
131
|
])
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('derives a new row id from its whole label, unique among the others', () => {
|
|
135
|
+
const stored = toStoredCategories(
|
|
136
|
+
[
|
|
137
|
+
row({ id: 'billing', label: 'Billing' }),
|
|
138
|
+
row({ label: 'Billing', isNew: true }),
|
|
139
|
+
row({ label: 'Été', isNew: true }),
|
|
140
|
+
],
|
|
141
|
+
'en',
|
|
142
|
+
)
|
|
108
143
|
expect(stored.map((category) => category.id)).toEqual([
|
|
109
144
|
'billing',
|
|
110
145
|
'billing-2',
|
|
@@ -113,13 +148,16 @@ describe('toStoredCategories', () => {
|
|
|
113
148
|
})
|
|
114
149
|
|
|
115
150
|
it('keeps a row without a label, for the server to name, and fills a missing icon', () => {
|
|
116
|
-
const stored = toStoredCategories(
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
151
|
+
const stored = toStoredCategories(
|
|
152
|
+
[
|
|
153
|
+
row({ label: ' ', isNew: true }),
|
|
154
|
+
row({ label: '!!!', isNew: true, icon: '' }),
|
|
155
|
+
],
|
|
156
|
+
'en',
|
|
157
|
+
)
|
|
120
158
|
expect(stored).toEqual([
|
|
121
|
-
{ id: 'category', label: '', icon: 'i-ph-package' },
|
|
122
|
-
{ id: 'category-2', label: '!!!', icon: 'i-ph-folder' },
|
|
159
|
+
{ id: 'category', label: { en: '' }, icon: 'i-ph-package' },
|
|
160
|
+
{ id: 'category-2', label: { en: '!!!' }, icon: 'i-ph-folder' },
|
|
123
161
|
])
|
|
124
162
|
})
|
|
125
163
|
})
|
|
@@ -151,7 +189,7 @@ describe('moveRow', () => {
|
|
|
151
189
|
})
|
|
152
190
|
|
|
153
191
|
describe('category moves', () => {
|
|
154
|
-
const gone = { id: 'gone', label: 'Gone', icon: 'i-ph-gift' }
|
|
192
|
+
const gone = { id: 'gone', label: { en: 'Gone' }, icon: 'i-ph-gift' }
|
|
155
193
|
|
|
156
194
|
it('hands the form the kept categories, then the moved ones', () => {
|
|
157
195
|
const entries = withMoves(CATEGORIES, [
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import {
|
|
3
|
-
describeCondition,
|
|
4
|
-
evaluateCondition,
|
|
5
|
-
} from '../app/utils/conditions'
|
|
2
|
+
import { describeCondition, evaluateCondition } from '../app/utils/conditions'
|
|
6
3
|
|
|
7
4
|
describe('conditions', () => {
|
|
8
5
|
it('describes with translated operator labels', () => {
|
|
@@ -114,4 +114,31 @@ describe('FunnelCard', () => {
|
|
|
114
114
|
expect(host.querySelectorAll('[data-funnel-untracked]')).toHaveLength(3)
|
|
115
115
|
expect(host.textContent).toContain('dms_mailing.funnel_card.untracked')
|
|
116
116
|
})
|
|
117
|
+
|
|
118
|
+
it('adds the change of each step while the toolbar compares', async () => {
|
|
119
|
+
chart.data.value = {
|
|
120
|
+
steps: [
|
|
121
|
+
{ ...funnelStep('sent', 30), previousCount: 20, delta: 50 },
|
|
122
|
+
{ ...funnelStep('delivered', 30), previousCount: 0 },
|
|
123
|
+
],
|
|
124
|
+
}
|
|
125
|
+
const { host } = mount(FunnelCard, {}, GLOBALS)
|
|
126
|
+
await flush()
|
|
127
|
+
|
|
128
|
+
const deltas = host.querySelectorAll('[data-funnel-delta]')
|
|
129
|
+
expect(deltas).toHaveLength(2)
|
|
130
|
+
expect(deltas[0]?.textContent?.trim()).toBe('▲ 50%')
|
|
131
|
+
expect(deltas[0]?.getAttribute('title')).toContain(
|
|
132
|
+
'dms_mailing.funnel_card.compared_count',
|
|
133
|
+
)
|
|
134
|
+
expect(deltas[1]?.textContent?.trim()).toBe('—')
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('drops the change column with the comparison off', async () => {
|
|
138
|
+
chart.data.value = { steps: [funnelStep('sent', 30)] }
|
|
139
|
+
const { host } = mount(FunnelCard, {}, GLOBALS)
|
|
140
|
+
await flush()
|
|
141
|
+
|
|
142
|
+
expect(host.querySelectorAll('[data-funnel-delta]')).toHaveLength(0)
|
|
143
|
+
})
|
|
117
144
|
})
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
2
|
import {
|
|
3
3
|
biggestDrop,
|
|
4
|
+
deltaTone,
|
|
5
|
+
formatDelta,
|
|
4
6
|
formatRate,
|
|
5
7
|
funnelTakeaway,
|
|
8
|
+
isCompared,
|
|
6
9
|
type FunnelStepReading,
|
|
7
10
|
} from '../app/utils/funnel'
|
|
8
11
|
|
|
@@ -79,3 +82,25 @@ describe('formatRate', () => {
|
|
|
79
82
|
expect(formatRate(null, 'en-GB')).toBe('—')
|
|
80
83
|
})
|
|
81
84
|
})
|
|
85
|
+
|
|
86
|
+
describe('the comparison of the steps', () => {
|
|
87
|
+
it('is on as soon as a step carries a compared count', () => {
|
|
88
|
+
expect(isCompared([step('sent', 4)])).toBe(false)
|
|
89
|
+
expect(isCompared([{ ...step('sent', 4), previousCount: 0 }])).toBe(true)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('prints the change with its arrow, a dash without one', () => {
|
|
93
|
+
expect(formatDelta(12.5, 'en-GB')).toBe('▲ 12.5%')
|
|
94
|
+
expect(formatDelta(-40, 'en-GB')).toBe('▼ 40%')
|
|
95
|
+
expect(formatDelta(0, 'en-GB')).toBe('0%')
|
|
96
|
+
expect(formatDelta(undefined, 'en-GB')).toBe('—')
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('reads growth as good down the funnel and bad for the leak', () => {
|
|
100
|
+
expect(deltaTone({ ...step('opened', 4), delta: 10 })).toBe('success')
|
|
101
|
+
expect(deltaTone({ ...step('opened', 4), delta: -10 })).toBe('error')
|
|
102
|
+
expect(deltaTone({ ...step('unsubscribed', 4), delta: 10 })).toBe('error')
|
|
103
|
+
expect(deltaTone({ ...step('unsubscribed', 4), delta: -10 })).toBe('success')
|
|
104
|
+
expect(deltaTone(step('opened', 4))).toBe('neutral')
|
|
105
|
+
})
|
|
106
|
+
})
|
|
@@ -1,40 +1,11 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import {
|
|
3
|
-
defaultCategoryLabelKey,
|
|
4
|
-
localizeCategories,
|
|
5
|
-
} from '../app/utils/categories'
|
|
6
2
|
import { localizeStarters, starterNameKey } from '../app/utils/starters'
|
|
7
3
|
|
|
8
4
|
const FRENCH: Record<string, string> = {
|
|
9
|
-
'dms_mailing.categories.defaults.orders': 'Commandes',
|
|
10
|
-
'dms_mailing.categories.defaults.billing': 'Facturation',
|
|
11
5
|
'dms_mailing.starters.names.order-confirmed': 'Confirmation de commande',
|
|
12
6
|
}
|
|
13
7
|
const translate = (key: string): string | undefined => FRENCH[key]
|
|
14
8
|
|
|
15
|
-
describe('localizeCategories', () => {
|
|
16
|
-
it('translates a shipped category nobody renamed', () => {
|
|
17
|
-
const [orders] = localizeCategories(
|
|
18
|
-
[{ id: 'orders', label: 'Orders', icon: 'i-ph-package' }],
|
|
19
|
-
translate,
|
|
20
|
-
)
|
|
21
|
-
expect(orders).toEqual({
|
|
22
|
-
id: 'orders',
|
|
23
|
-
label: 'Commandes',
|
|
24
|
-
icon: 'i-ph-package',
|
|
25
|
-
})
|
|
26
|
-
})
|
|
27
|
-
it('keeps a label someone wrote, and a category it cannot translate', () => {
|
|
28
|
-
const categories = [
|
|
29
|
-
{ id: 'billing', label: 'Invoices & receipts', icon: 'i-ph-file-text' },
|
|
30
|
-
{ id: 'forms', label: 'Forms', icon: 'i-ph-textbox' },
|
|
31
|
-
{ id: 'news', label: 'News', icon: 'i-ph-newspaper' },
|
|
32
|
-
]
|
|
33
|
-
expect(localizeCategories(categories, translate)).toEqual(categories)
|
|
34
|
-
expect(defaultCategoryLabelKey(categories[2]!)).toBeUndefined()
|
|
35
|
-
})
|
|
36
|
-
})
|
|
37
|
-
|
|
38
9
|
describe('localizeStarters', () => {
|
|
39
10
|
it('names a known starter in the interface language, else as shipped', () => {
|
|
40
11
|
const starters = [
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
3
3
|
import { createApp, defineComponent, h, onBeforeUnmount } from 'vue'
|
|
4
4
|
import OverviewGate from '../app/components/OverviewGate.vue'
|
|
5
|
-
import { useSetupHidesPeriod } from '../app/composables/useOverviewSetup'
|
|
6
5
|
import type { SetupFacts } from '../app/utils/overview-setup'
|
|
7
6
|
import { stubApiData, type ApiDataStub } from './helpers/api-data'
|
|
8
7
|
import { flush, mount, slotStub, stubNuxtGlobals } from './helpers/nuxt-globals'
|
|
@@ -57,15 +56,6 @@ const Gate = defineComponent({
|
|
|
57
56
|
setup: () => () => h(OverviewGate, null, { default: () => h(Dashboard) }),
|
|
58
57
|
})
|
|
59
58
|
|
|
60
|
-
/** What the page header renders of the period selector. */
|
|
61
|
-
const PeriodHolder = defineComponent({
|
|
62
|
-
props: { scope: { type: String, default: undefined } },
|
|
63
|
-
setup: (props) => {
|
|
64
|
-
const hidesPeriod = useSetupHidesPeriod(props.scope)
|
|
65
|
-
return () => h('span', { 'data-hides-period': String(hidesPeriod.value) })
|
|
66
|
-
},
|
|
67
|
-
})
|
|
68
|
-
|
|
69
59
|
const GLOBALS = {
|
|
70
60
|
DmsCard: slotStub('DmsCard'),
|
|
71
61
|
DmsEmptyState: EmptyStateStub,
|
|
@@ -88,9 +78,6 @@ const facts = (overrides: Partial<SetupFacts> = {}): SetupFacts => ({
|
|
|
88
78
|
beforeEach(() => {
|
|
89
79
|
stubNuxtGlobals()
|
|
90
80
|
vi.stubGlobal('onBeforeUnmount', onBeforeUnmount)
|
|
91
|
-
vi.stubGlobal('holdPageHeaderActions', () =>
|
|
92
|
-
defineComponent({ setup: () => () => null }),
|
|
93
|
-
)
|
|
94
81
|
api = stubApiData()
|
|
95
82
|
})
|
|
96
83
|
|
|
@@ -119,7 +106,7 @@ describe('OverviewGate', () => {
|
|
|
119
106
|
expect(host.querySelector('[data-dashboard]')).not.toBeNull()
|
|
120
107
|
})
|
|
121
108
|
|
|
122
|
-
it('holds the dashboard
|
|
109
|
+
it('holds the dashboard back while nothing can be sent', async () => {
|
|
123
110
|
api.respond(() => facts({ liveTemplateCount: 0 }))
|
|
124
111
|
const { host } = mount(Gate, {}, GLOBALS)
|
|
125
112
|
await flush()
|
|
@@ -127,16 +114,6 @@ describe('OverviewGate', () => {
|
|
|
127
114
|
expect(host.querySelectorAll('[data-step]')).toHaveLength(4)
|
|
128
115
|
})
|
|
129
116
|
|
|
130
|
-
it('hides the overview header period from the same facts, on no other page', async () => {
|
|
131
|
-
api.respond(() => facts({ liveTemplateCount: 0 }))
|
|
132
|
-
const overview = mount(PeriodHolder, { scope: 'mailing-overview' })
|
|
133
|
-
const sends = mount(PeriodHolder, { scope: 'mailing-sends' })
|
|
134
|
-
await flush()
|
|
135
|
-
expect(overview.host.innerHTML).toContain('data-hides-period="true"')
|
|
136
|
-
expect(sends.host.innerHTML).toContain('data-hides-period="false"')
|
|
137
|
-
expect(api.reads).toEqual([{ url: SETUP_URL, query: {} }])
|
|
138
|
-
})
|
|
139
|
-
|
|
140
117
|
it('lets an unfinished setup be resumed above the dashboard', async () => {
|
|
141
118
|
api.respond(() => facts({ hasTestSend: false }))
|
|
142
119
|
const { host } = mount(Gate, {}, GLOBALS)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @vitest-environment jsdom
|
|
2
2
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
3
|
-
import { defineComponent, h
|
|
4
|
-
import
|
|
3
|
+
import { defineComponent, h } from 'vue'
|
|
4
|
+
import ProviderPill from '../app/components/ProviderPill.vue'
|
|
5
5
|
import WebhookStatus from '../app/components/WebhookStatus.vue'
|
|
6
6
|
import type { WebhookStatus as Status } from '../app/utils/webhook-status'
|
|
7
7
|
import {
|
|
@@ -38,6 +38,8 @@ const PillStub = defineComponent({
|
|
|
38
38
|
setup: (props) => () => h('span', { 'data-pill': props.label }),
|
|
39
39
|
})
|
|
40
40
|
|
|
41
|
+
const PILL_GLOBALS = { DmsStatusPill: PillStub }
|
|
42
|
+
|
|
41
43
|
const ButtonStub = defineComponent({
|
|
42
44
|
props: { label: { type: String, default: '' } },
|
|
43
45
|
emits: ['click'],
|
|
@@ -48,29 +50,11 @@ const ButtonStub = defineComponent({
|
|
|
48
50
|
})
|
|
49
51
|
|
|
50
52
|
let api$: ApiDataStub
|
|
51
|
-
let headerActions: (() => VNodeChild) | null
|
|
52
|
-
|
|
53
|
-
/** The page header: renders the actions `PageHeader` set up. */
|
|
54
|
-
const Header = defineComponent({
|
|
55
|
-
setup: () => () => h('header', headerActions?.()),
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
const HeaderOutlet = defineComponent({
|
|
59
|
-
props: { showProvider: { type: Boolean, default: true } },
|
|
60
|
-
setup: (props) => () =>
|
|
61
|
-
h('div', [h(PageHeader, { showProvider: props.showProvider }), h(Header)]),
|
|
62
|
-
})
|
|
63
53
|
|
|
64
54
|
beforeEach(() => {
|
|
65
55
|
stubNuxtGlobals()
|
|
66
56
|
stubRegionalFormat()
|
|
67
|
-
|
|
68
|
-
vi.stubGlobal('resolveComponent', (name: string) =>
|
|
69
|
-
name === 'DmsStatusPill' ? PillStub : slotStub(name),
|
|
70
|
-
)
|
|
71
|
-
vi.stubGlobal('usePageHeaderActions', (render: () => VNodeChild) => {
|
|
72
|
-
headerActions = render
|
|
73
|
-
})
|
|
57
|
+
vi.stubGlobal('resolveComponent', (name: string) => slotStub(name))
|
|
74
58
|
vi.stubGlobal('useApiError', vi.fn())
|
|
75
59
|
api$ = stubApiData()
|
|
76
60
|
api.webhookTest.mockReset()
|
|
@@ -81,10 +65,10 @@ afterEach(() => {
|
|
|
81
65
|
document.body.innerHTML = ''
|
|
82
66
|
})
|
|
83
67
|
|
|
84
|
-
describe('
|
|
68
|
+
describe('ProviderPill', () => {
|
|
85
69
|
it('names the provider read with the page', async () => {
|
|
86
70
|
api$.respond(() => ({ name: 'smtp', connected: true, features: null }))
|
|
87
|
-
const view = mount(
|
|
71
|
+
const view = mount(ProviderPill, {}, PILL_GLOBALS)
|
|
88
72
|
await flush()
|
|
89
73
|
expect(api$.reads).toEqual([{ url: PROVIDER_URL, query: {} }])
|
|
90
74
|
expect(
|
|
@@ -95,14 +79,14 @@ describe('PageHeader', () => {
|
|
|
95
79
|
|
|
96
80
|
it('says no provider when the read fails, and reads nothing when hidden', async () => {
|
|
97
81
|
api$.respond(() => Promise.reject(new Error('503')))
|
|
98
|
-
const failed = mount(
|
|
82
|
+
const failed = mount(ProviderPill, {}, PILL_GLOBALS)
|
|
99
83
|
await flush()
|
|
100
84
|
expect(
|
|
101
85
|
failed.host.querySelector('[data-pill]')?.getAttribute('data-pill'),
|
|
102
86
|
).toBe('dms_mailing.page_header.no_provider')
|
|
103
87
|
failed.unmount()
|
|
104
88
|
api$.reads.length = 0
|
|
105
|
-
const hidden = mount(
|
|
89
|
+
const hidden = mount(ProviderPill, { showProvider: false }, PILL_GLOBALS)
|
|
106
90
|
await flush()
|
|
107
91
|
expect(api$.reads).toEqual([])
|
|
108
92
|
expect(hidden.host.querySelector('[data-pill]')).toBeNull()
|
|
@@ -3,22 +3,10 @@ import {
|
|
|
3
3
|
clampRetention,
|
|
4
4
|
isWithinADay,
|
|
5
5
|
minutesSince,
|
|
6
|
-
presetShortLabelKey,
|
|
7
6
|
RETENTION_CUSTOM,
|
|
8
7
|
retentionChoice,
|
|
9
8
|
} from '../app/utils/period'
|
|
10
9
|
|
|
11
|
-
describe('presetShortLabelKey', () => {
|
|
12
|
-
it('maps a preset id onto an i18n key', () => {
|
|
13
|
-
expect(presetShortLabelKey('last-24h')).toBe(
|
|
14
|
-
'dms_mailing.period.short.last_24h',
|
|
15
|
-
)
|
|
16
|
-
expect(presetShortLabelKey('last-30-days')).toBe(
|
|
17
|
-
'dms_mailing.period.short.last_30_days',
|
|
18
|
-
)
|
|
19
|
-
})
|
|
20
|
-
})
|
|
21
|
-
|
|
22
10
|
describe('minutesSince', () => {
|
|
23
11
|
const now = new Date('2026-09-09T10:35:00.000Z')
|
|
24
12
|
it('counts whole minutes', () => {
|