@dev.smartpricing/message-composer-layer 1.0.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.
Files changed (169) hide show
  1. package/README.md +75 -0
  2. package/app/api/ai.ts +28 -0
  3. package/app/api/brands.ts +54 -0
  4. package/app/api/client.ts +56 -0
  5. package/app/api/compilation.ts +34 -0
  6. package/app/api/dynamicValues.ts +11 -0
  7. package/app/api/index.ts +11 -0
  8. package/app/api/languages.ts +6 -0
  9. package/app/api/media.ts +26 -0
  10. package/app/api/messageGroups.ts +49 -0
  11. package/app/api/messages.ts +22 -0
  12. package/app/api/meta.ts +18 -0
  13. package/app/api/tracking.ts +9 -0
  14. package/app/components/Brand/BrandDrawer.vue +281 -0
  15. package/app/components/Brand/BrandPreview.vue +137 -0
  16. package/app/components/Brand/Steps/BrandStepGeneral.vue +37 -0
  17. package/app/components/Brand/Steps/BrandStepSocials.vue +48 -0
  18. package/app/components/Brand/Steps/BrandStepVisual.vue +214 -0
  19. package/app/components/Common/BrandColorSwatches.vue +37 -0
  20. package/app/components/Common/ColorInput.vue +144 -0
  21. package/app/components/Common/EmojiPickerButton.vue +35 -0
  22. package/app/components/Common/FontFamilySelect.vue +39 -0
  23. package/app/components/Common/HtmlPreviewIframe.vue +68 -0
  24. package/app/components/Common/IncompleteTranslationsDialog.vue +97 -0
  25. package/app/components/Common/LanguageList.vue +356 -0
  26. package/app/components/Common/LoadingOverlay.vue +12 -0
  27. package/app/components/Common/MediaUploadOrSelect.vue +299 -0
  28. package/app/components/Common/MessagePreviewSlideover.vue +88 -0
  29. package/app/components/Common/MonacoEditor.vue +110 -0
  30. package/app/components/Common/PhoneInput.vue +804 -0
  31. package/app/components/Common/SectionHeader.vue +26 -0
  32. package/app/components/Common/TranslationActionBar.vue +139 -0
  33. package/app/components/Common/ViewLessMore.vue +40 -0
  34. package/app/components/CreateTemplateButton.vue +59 -0
  35. package/app/components/DynamicDataButton.vue +75 -0
  36. package/app/components/DynamicDataSlideover.vue +149 -0
  37. package/app/components/Editor/AiGenerateSlideover.vue +292 -0
  38. package/app/components/Editor/EditorLinkPopover.vue +154 -0
  39. package/app/components/Editor/RichTextEditor.vue +495 -0
  40. package/app/components/Email/Editor/BlockLibrary.vue +89 -0
  41. package/app/components/Email/Editor/BlockSettings.vue +60 -0
  42. package/app/components/Email/Editor/Blocks/ButtonBlock.vue +117 -0
  43. package/app/components/Email/Editor/Blocks/ButtonSettings.vue +228 -0
  44. package/app/components/Email/Editor/Blocks/DiscountCodeBlock.vue +74 -0
  45. package/app/components/Email/Editor/Blocks/DiscountCodeSettings.vue +162 -0
  46. package/app/components/Email/Editor/Blocks/DividerBlock.vue +31 -0
  47. package/app/components/Email/Editor/Blocks/DividerSettings.vue +108 -0
  48. package/app/components/Email/Editor/Blocks/FooterBlock.vue +61 -0
  49. package/app/components/Email/Editor/Blocks/FooterSettings.vue +123 -0
  50. package/app/components/Email/Editor/Blocks/GridBlock.vue +128 -0
  51. package/app/components/Email/Editor/Blocks/GridSettings.vue +109 -0
  52. package/app/components/Email/Editor/Blocks/HTMLBlock.vue +31 -0
  53. package/app/components/Email/Editor/Blocks/HTMLSettings.vue +85 -0
  54. package/app/components/Email/Editor/Blocks/HeaderBlock.vue +53 -0
  55. package/app/components/Email/Editor/Blocks/HeaderSettings.vue +113 -0
  56. package/app/components/Email/Editor/Blocks/HeadingBlock.vue +61 -0
  57. package/app/components/Email/Editor/Blocks/HeadingSettings.vue +156 -0
  58. package/app/components/Email/Editor/Blocks/ImageBlock.vue +49 -0
  59. package/app/components/Email/Editor/Blocks/ImageSettings.vue +53 -0
  60. package/app/components/Email/Editor/Blocks/IndexBlock.vue +155 -0
  61. package/app/components/Email/Editor/Blocks/ParagraphBlock.vue +61 -0
  62. package/app/components/Email/Editor/Blocks/ParagraphSettings.vue +157 -0
  63. package/app/components/Email/Editor/Blocks/SocialsBlock.vue +63 -0
  64. package/app/components/Email/Editor/Blocks/SocialsSettings.vue +117 -0
  65. package/app/components/Email/Editor/Blocks/SpacerBlock.vue +37 -0
  66. package/app/components/Email/Editor/Blocks/SpacerSettings.vue +95 -0
  67. package/app/components/Email/Editor/Blocks/TopProductsBlock.vue +47 -0
  68. package/app/components/Email/Editor/Blocks/TopProductsSettings.vue +87 -0
  69. package/app/components/Email/Editor/Blocks/UnsubscribeLinkBlock.vue +61 -0
  70. package/app/components/Email/Editor/Blocks/UnsubscribeLinkSettings.vue +123 -0
  71. package/app/components/Email/Editor/Canvas.vue +143 -0
  72. package/app/components/Email/Editor/GeneralStyleSettings.vue +125 -0
  73. package/app/components/Email/Editor/Index.vue +77 -0
  74. package/app/components/Email/Editor/LeftSidebar.vue +95 -0
  75. package/app/components/Email/InlinePreview.vue +124 -0
  76. package/app/components/Email/Preview.vue +126 -0
  77. package/app/components/Email/PreviewPanel.vue +57 -0
  78. package/app/components/Email/SendTestEmailModal.vue +121 -0
  79. package/app/components/Email/TranslationEditor.vue +198 -0
  80. package/app/components/Email/Translator/Button.vue +101 -0
  81. package/app/components/Email/Translator/Discount.vue +60 -0
  82. package/app/components/Email/Translator/Footer.vue +66 -0
  83. package/app/components/Email/Translator/Grid.vue +127 -0
  84. package/app/components/Email/Translator/Header.vue +90 -0
  85. package/app/components/Email/Translator/Heading.vue +66 -0
  86. package/app/components/Email/Translator/Image.vue +91 -0
  87. package/app/components/Email/Translator/Index.vue +138 -0
  88. package/app/components/Email/Translator/Paragraph.vue +66 -0
  89. package/app/components/Email/Translator/RawHtml.vue +64 -0
  90. package/app/components/Email/Translator/UnsubscribeLink.vue +59 -0
  91. package/app/components/TemplateList/BaseTable.vue +354 -0
  92. package/app/components/TemplateList/StatusCell.vue +67 -0
  93. package/app/components/Whatsapp/CategoryChanger.vue +63 -0
  94. package/app/components/Whatsapp/Editor/Body.vue +53 -0
  95. package/app/components/Whatsapp/Editor/Buttons.vue +341 -0
  96. package/app/components/Whatsapp/Editor/Footer.vue +70 -0
  97. package/app/components/Whatsapp/Editor/Header.vue +156 -0
  98. package/app/components/Whatsapp/Editor/Index.vue +89 -0
  99. package/app/components/Whatsapp/Editor/SyncWrapper.vue +166 -0
  100. package/app/components/Whatsapp/Preview.vue +197 -0
  101. package/app/components/Whatsapp/PreviewPanel.vue +24 -0
  102. package/app/components/Whatsapp/TranslationEditor.vue +150 -0
  103. package/app/components/Whatsapp/Translator/Body.vue +61 -0
  104. package/app/components/Whatsapp/Translator/Button.vue +209 -0
  105. package/app/components/Whatsapp/Translator/Footer.vue +56 -0
  106. package/app/components/Whatsapp/Translator/Header.vue +61 -0
  107. package/app/components/Whatsapp/Translator/HeaderMedia.vue +117 -0
  108. package/app/components/Whatsapp/Translator/Index.vue +159 -0
  109. package/app/composables/composerContext.ts +35 -0
  110. package/app/composables/useAutoTranslate.ts +242 -0
  111. package/app/composables/useAutoTranslateEmail.ts +294 -0
  112. package/app/composables/useChangeTracking.ts +212 -0
  113. package/app/composables/useChangeTrackingEmail.ts +227 -0
  114. package/app/composables/useChannels.ts +36 -0
  115. package/app/composables/useEmailBlockContext.ts +43 -0
  116. package/app/composables/useExitConfirmation.ts +39 -0
  117. package/app/composables/useGoBack.ts +15 -0
  118. package/app/composables/useIncompleteTranslations.ts +55 -0
  119. package/app/composables/useIncompleteTranslationsEmail.ts +56 -0
  120. package/app/composables/useMediaLimits.ts +42 -0
  121. package/app/composables/useTemplateFormValidation.ts +109 -0
  122. package/app/composables/useTemplateOperations.ts +147 -0
  123. package/app/composables/useTracking.ts +39 -0
  124. package/app/composables/useTranslationFilters.ts +176 -0
  125. package/app/composables/useTranslationFiltersEmail.ts +244 -0
  126. package/app/composables/useWhatsappTemplateBehaviors.ts +58 -0
  127. package/app/middleware/channel-guard.ts +13 -0
  128. package/app/pages/brands.vue +195 -0
  129. package/app/pages/images.vue +141 -0
  130. package/app/pages/index.vue +198 -0
  131. package/app/pages/preview/[id].vue +47 -0
  132. package/app/pages/template/email/[id].vue +596 -0
  133. package/app/pages/template/email/create.vue +562 -0
  134. package/app/pages/template/whatsapp/[id].vue +561 -0
  135. package/app/pages/template/whatsapp/create.vue +468 -0
  136. package/app/plugins/admin-shortcut.client.ts +7 -0
  137. package/app/plugins/tracking.client.ts +12 -0
  138. package/app/queries/ai.ts +20 -0
  139. package/app/queries/brands.ts +75 -0
  140. package/app/queries/compilation.ts +37 -0
  141. package/app/queries/dynamicValues.ts +13 -0
  142. package/app/queries/index.ts +9 -0
  143. package/app/queries/languages.ts +10 -0
  144. package/app/queries/media.ts +27 -0
  145. package/app/queries/messageGroups.ts +112 -0
  146. package/app/queries/messages.ts +42 -0
  147. package/app/queries/meta.ts +13 -0
  148. package/app/stores/adminStore.ts +50 -0
  149. package/app/stores/appContext.ts +99 -0
  150. package/app/stores/emailEditor.ts +220 -0
  151. package/app/stores/main.ts +3 -0
  152. package/app/stores/metaSubmission.ts +116 -0
  153. package/app/types/app-config.d.ts +14 -0
  154. package/app/types/hooks.d.ts +9 -0
  155. package/app/types/tracking.ts +148 -0
  156. package/app/utils/locale.ts +17 -0
  157. package/app/utils/notifyParent.ts +7 -0
  158. package/app/utils/resolveDefaultLanguage.ts +14 -0
  159. package/app/utils/socialDefaults.ts +42 -0
  160. package/app/utils/testIds.const.ts +728 -0
  161. package/app.config.ts +5 -0
  162. package/i18n/check-translations.ts +56 -0
  163. package/i18n/locales/de.ts +834 -0
  164. package/i18n/locales/en.ts +826 -0
  165. package/i18n/locales/es.ts +833 -0
  166. package/i18n/locales/fr.ts +837 -0
  167. package/i18n/locales/it.ts +831 -0
  168. package/nuxt.config.ts +65 -0
  169. package/package.json +64 -0
@@ -0,0 +1,562 @@
1
+ <script setup lang="ts">
2
+ definePageMeta({
3
+ middleware: ['channel-guard'],
4
+ })
5
+
6
+ import EmailEditor from '@/components/Email/Editor/Index.vue'
7
+ import TranslationEditor from '@/components/Email/TranslationEditor.vue'
8
+ import LanguageList from '@/components/Common/LanguageList.vue'
9
+ import IncompleteTranslationsDialog from '@/components/Common/IncompleteTranslationsDialog.vue'
10
+ import { computed, ref, watch, onMounted, onUnmounted } from 'vue'
11
+ import {
12
+ type MessageGroup,
13
+ type MessageGroupCategory,
14
+ type PostEntity,
15
+ type EmailMessage,
16
+ } from '@dev.smartpricing/message-composer-utils/types'
17
+ import {
18
+ getEmailEmpty,
19
+ duplicateBlocksForNewLanguage,
20
+ syncLayoutFromBase,
21
+ } from '@dev.smartpricing/message-composer-utils/utils'
22
+ import { useAppContextStore } from '@/stores/appContext'
23
+ import { useLanguagesQuery } from '~/queries'
24
+
25
+ const { trackEvent } = useTracking()
26
+ const { mainLanguage } = storeToRefs(useAppContextStore())
27
+
28
+ const adminStore = useAdminStore()
29
+
30
+ const name = ref('')
31
+ const category = ref<MessageGroupCategory>('marketing')
32
+ const isInternal = ref(false)
33
+ const language = ref(mainLanguage.value)
34
+ const targetLanguage = ref(mainLanguage.value)
35
+ const activeTab = ref<'content' | 'translations'>('content')
36
+ const baseLanguage = ref(mainLanguage.value)
37
+ const { goBackOrFallback } = useGoBack()
38
+ const emailEditorStore = useEmailEditorStore()
39
+
40
+ const { saving, createTemplate } = useTemplateOperations({
41
+ onSuccess: () => {
42
+ created.value = true
43
+ goBackOrFallback()
44
+ },
45
+ })
46
+
47
+ const messageGroup = computed(() => {
48
+ return {
49
+ user_id: '',
50
+ name: name.value,
51
+ category: category.value,
52
+ visibility: isInternal.value ? 'internal' : 'public',
53
+ brand_id: emailEditorStore.brandId,
54
+ } satisfies PostEntity<MessageGroup>
55
+ })
56
+
57
+ const messages = ref<Record<string, PostEntity<EmailMessage>>>({
58
+ [mainLanguage.value]: getEmailEmpty(mainLanguage.value),
59
+ })
60
+
61
+ const { formState, draftSchema, validationErrors, validateDraft, validateFull, clearErrors } =
62
+ useTemplateFormValidation({
63
+ type: 'email',
64
+ name,
65
+ category,
66
+ messages,
67
+ })
68
+
69
+ // Initialize store with empty email on mount
70
+ onMounted(() => {
71
+ const currentMessage = messages.value[language.value]
72
+ if (currentMessage) {
73
+ emailEditorStore.loadFromData(currentMessage)
74
+ }
75
+ })
76
+
77
+ // Set oldest brand as default
78
+ emailEditorStore.initBrand()
79
+
80
+ // Watch language changes to load the correct message
81
+ watch(language, (newLang) => {
82
+ if (!messages.value[newLang]) {
83
+ // Create new language message by duplicating blocks from base language
84
+ const baseMessage = messages.value[baseLanguage.value]
85
+ if (baseMessage && baseMessage.body.blocks.length > 0) {
86
+ const newMessage = getEmailEmpty(newLang)
87
+ newMessage.body.blocks = duplicateBlocksForNewLanguage(baseMessage.body.blocks)
88
+ newMessage.body.settings = { ...baseMessage.body.settings }
89
+ messages.value[newLang] = newMessage
90
+ } else {
91
+ messages.value[newLang] = getEmailEmpty(newLang)
92
+ }
93
+ }
94
+
95
+ const message = messages.value[newLang]
96
+ if (message) {
97
+ emailEditorStore.loadFromData(message)
98
+ }
99
+ })
100
+
101
+ // Sync all languages when switching to translations tab
102
+ watch(activeTab, (newTab) => {
103
+ if (newTab === 'translations') {
104
+ const baseMessage = messages.value[language.value]
105
+ if (!baseMessage || baseMessage.render_type !== 'email_v1') return
106
+
107
+ for (const [langId, message] of Object.entries(messages.value)) {
108
+ if (langId !== language.value && message.render_type === 'email_v1') {
109
+ message.body.blocks = syncLayoutFromBase(baseMessage.body.blocks, message.body.blocks)
110
+ message.body.settings = { ...baseMessage.body.settings }
111
+ }
112
+ }
113
+ }
114
+ })
115
+
116
+ // Set initial target language when languages are added
117
+ watch(
118
+ () => Object.keys(messages.value).length,
119
+ () => {
120
+ const languages = Object.keys(messages.value)
121
+ if (languages.length > 1 && targetLanguage.value === language.value) {
122
+ targetLanguage.value = languages.find((l) => l !== language.value) || language.value
123
+ }
124
+ },
125
+ )
126
+
127
+ // Sync store changes back to messages
128
+ watch(
129
+ () => emailEditorStore.blocks,
130
+ () => {
131
+ const currentMessage = messages.value[language.value]
132
+ if (currentMessage) {
133
+ const editorData = emailEditorStore.toJSON()
134
+ currentMessage.body.blocks = editorData.blocks
135
+ currentMessage.body.settings = editorData.settings
136
+ }
137
+ },
138
+ { deep: true },
139
+ )
140
+
141
+ const hasAnyChanges = computed(() => {
142
+ // For new templates, consider it changed if name or any content exists
143
+ return name.value.trim() !== '' || emailEditorStore.hasBlocks
144
+ })
145
+
146
+ const created = ref(false)
147
+
148
+ useExitConfirmation(computed(() => hasAnyChanges.value && !created.value))
149
+
150
+ // Get languages for incomplete translations check
151
+ const { data: languages } = useLanguagesQuery()
152
+
153
+ // Check for incomplete translations
154
+ const { incompleteLanguages, hasIncompleteTranslations } = useIncompleteTranslationsEmail({
155
+ messages,
156
+ languages,
157
+ })
158
+
159
+ // Dialog state and pending action
160
+ const showIncompleteDialog = ref(false)
161
+ const pendingAction = ref<'save_draft' | 'save_message' | null>(null)
162
+
163
+ // Auto-translate composable
164
+ const { translateMessage, isTranslating } = useAutoTranslateEmail()
165
+
166
+ // Cleanup on unmount
167
+ onUnmounted(() => {
168
+ emailEditorStore.resetEditor()
169
+ })
170
+
171
+ // ── Tracking helpers ──
172
+ function getEmailBaseProps() {
173
+ return { name: name.value, subject: messages.value[language.value]?.body?.subject ?? '' }
174
+ }
175
+
176
+ function getEmailDraftProps() {
177
+ return {
178
+ ...getEmailBaseProps(),
179
+ languages: Object.keys(messages.value),
180
+ widgets: emailEditorStore.blocks.map((b) => b.type),
181
+ is_internal: isInternal.value,
182
+ }
183
+ }
184
+
185
+ watch(activeTab, (tab) => {
186
+ trackEvent('email_create.change_tab', { ...getEmailBaseProps(), tab })
187
+ })
188
+
189
+ function onAddLanguage(lang: string) {
190
+ trackEvent('email_create.add_language', { ...getEmailBaseProps(), language: lang })
191
+ }
192
+
193
+ function onRemoveLanguage(lang: string) {
194
+ trackEvent('email_create.remove_language', {
195
+ ...getEmailBaseProps(),
196
+ language: lang,
197
+ })
198
+ }
199
+
200
+ function onAutoTranslateTracked(lang: string) {
201
+ trackEvent('email_create.auto_translate', {
202
+ ...getEmailBaseProps(),
203
+ language: lang,
204
+ })
205
+ }
206
+
207
+ function onPreview(lang: string) {
208
+ trackEvent('email_create.preview', { ...getEmailBaseProps(), language: lang })
209
+ }
210
+
211
+ function onFiltersChanged(payload: {
212
+ language: string
213
+ search_term: string
214
+ show_only_missing: boolean
215
+ show_only_changes: boolean
216
+ }) {
217
+ trackEvent('email_create.translations_filters', { ...getEmailBaseProps(), ...payload })
218
+ }
219
+
220
+ function onBlockAdded(payload: { type: string; adding_mode: string }) {
221
+ trackEvent('email_create.add_widget', {
222
+ ...getEmailBaseProps(),
223
+ widget_type: payload.type,
224
+ adding_mode: payload.adding_mode,
225
+ })
226
+ }
227
+
228
+ function onBlockDuplicated(payload: { type: string }) {
229
+ trackEvent('email_create.duplicate_widget', {
230
+ ...getEmailBaseProps(),
231
+ widget_type: payload.type,
232
+ })
233
+ }
234
+
235
+ function onBlockDeleted(payload: { type: string }) {
236
+ trackEvent('email_create.remove_widget', {
237
+ ...getEmailBaseProps(),
238
+ widget_type: payload.type,
239
+ })
240
+ }
241
+
242
+ function onBlockSorted(payload: {
243
+ type: string
244
+ before_sort_position: number
245
+ new_position: number
246
+ }) {
247
+ trackEvent('email_create.sort_widget', {
248
+ ...getEmailBaseProps(),
249
+ widget_type: payload.type,
250
+ before_sort_position: payload.before_sort_position,
251
+ new_position: payload.new_position,
252
+ })
253
+ }
254
+
255
+ function onWidgetTabChanged(tab: string) {
256
+ trackEvent('email_create.change_widget_tab', { ...getEmailBaseProps(), tab })
257
+ }
258
+
259
+ function onTestSent(payload: { to_recipients: number }, lang?: string) {
260
+ trackEvent('email_create.send_test', {
261
+ ...getEmailBaseProps(),
262
+ widgets: emailEditorStore.blocks.map((b) => b.type),
263
+ to_recipients: payload.to_recipients,
264
+ language: lang ?? language.value,
265
+ })
266
+ }
267
+
268
+ // Sync all languages from base before save
269
+ function syncAllLanguagesBeforeSave() {
270
+ const baseMessage = messages.value[language.value]
271
+ if (!baseMessage || baseMessage.render_type !== 'email_v1') return
272
+
273
+ for (const [langId, message] of Object.entries(messages.value)) {
274
+ if (langId !== language.value && message.render_type === 'email_v1') {
275
+ message.body.blocks = syncLayoutFromBase(baseMessage.body.blocks, message.body.blocks)
276
+ message.body.settings = { ...baseMessage.body.settings }
277
+ }
278
+ }
279
+ }
280
+
281
+ async function saveAsDraft() {
282
+ trackEvent('email_create.save_draft', getEmailDraftProps())
283
+ syncAllLanguagesBeforeSave()
284
+
285
+ const templateData = {
286
+ messageGroup: messageGroup.value,
287
+ messages: messages.value,
288
+ }
289
+ const result = await createTemplate(templateData, 'draft')
290
+ if (result) {
291
+ notifyParent('TEMPLATE_SAVED_DRAFT', { id: result.id, name: result.name })
292
+ }
293
+ }
294
+
295
+ async function saveTemplate() {
296
+ trackEvent('email_create.save', getEmailDraftProps())
297
+ syncAllLanguagesBeforeSave()
298
+
299
+ const templateData = {
300
+ messageGroup: messageGroup.value,
301
+ messages: messages.value,
302
+ }
303
+ const result = await createTemplate(templateData, 'approved')
304
+ if (result) {
305
+ notifyParent('TEMPLATE_SAVED', { id: result.id, name: result.name, status: 'approved' })
306
+ }
307
+ }
308
+
309
+ // Wrapper functions to validate then check for incomplete translations
310
+ async function handleSaveAsDraft() {
311
+ if (!(await validateDraft())) return
312
+ if (hasIncompleteTranslations.value) {
313
+ pendingAction.value = 'save_draft'
314
+ showIncompleteDialog.value = true
315
+ } else {
316
+ saveAsDraft()
317
+ }
318
+ }
319
+
320
+ async function handleSaveTemplate() {
321
+ if (!(await validateFull())) return
322
+ if (hasIncompleteTranslations.value) {
323
+ pendingAction.value = 'save_message'
324
+ showIncompleteDialog.value = true
325
+ } else {
326
+ saveTemplate()
327
+ }
328
+ }
329
+
330
+ // Proceed with save without translating
331
+ function proceedWithSave() {
332
+ if (pendingAction.value === 'save_draft') {
333
+ saveAsDraft()
334
+ } else if (pendingAction.value === 'save_message') {
335
+ saveTemplate()
336
+ }
337
+ pendingAction.value = null
338
+ }
339
+
340
+ // Go to translations tab
341
+ function goToTranslations() {
342
+ activeTab.value = 'translations'
343
+ pendingAction.value = null
344
+ }
345
+
346
+ // Translate automatically and save
347
+ async function translateAndSave() {
348
+ if (!incompleteLanguages.value.length) {
349
+ proceedWithSave()
350
+ return
351
+ }
352
+
353
+ const baseMessage = messages.value[baseLanguage.value]
354
+ if (!baseMessage) {
355
+ proceedWithSave()
356
+ return
357
+ }
358
+
359
+ // Translate all incomplete languages
360
+ for (const { language: lang } of incompleteLanguages.value) {
361
+ const targetMessage = messages.value[lang.id]
362
+ if (!targetMessage) continue
363
+
364
+ await translateMessage(
365
+ baseMessage as EmailMessage,
366
+ targetMessage as EmailMessage,
367
+ baseLanguage.value,
368
+ lang.id,
369
+ )
370
+ }
371
+
372
+ // After translation, proceed with save
373
+ proceedWithSave()
374
+ }
375
+ </script>
376
+
377
+ <template>
378
+ <SNavigationPage>
379
+ <template #header>
380
+ <SNavigationBarHeader
381
+ :title="$t('pages.email.create_message_group.title')"
382
+ :tabs="[
383
+ {
384
+ label: $t('pages.email.create_message_group.tab_content'),
385
+ value: 'content',
386
+ },
387
+ {
388
+ label: $t('pages.email.create_message_group.tab_translations'),
389
+ value: 'translations',
390
+ },
391
+ ]"
392
+ :active-tab="activeTab"
393
+ :data-testid="templateEmailCreateTestIds.navbar"
394
+ @tab-change="activeTab = $event"
395
+ >
396
+ <template #actions>
397
+ <UButton
398
+ @click="
399
+ () => {
400
+ notifyParent('TEMPLATE_CANCELLED')
401
+ goBackOrFallback()
402
+ }
403
+ "
404
+ :label="$t('common.actions.cancel')"
405
+ color="primary"
406
+ variant="ghost"
407
+ :loading="saving || isTranslating"
408
+ :data-testid="templateEmailCreateTestIds.cancelButton"
409
+ />
410
+ <UButton
411
+ @click="handleSaveAsDraft"
412
+ :label="$t('pages.email.create_message_group.save_as_draft')"
413
+ color="primary"
414
+ variant="outline"
415
+ :loading="saving || isTranslating"
416
+ :data-testid="templateEmailCreateTestIds.saveDraftButton"
417
+ />
418
+ <UButton
419
+ @click="handleSaveTemplate"
420
+ :label="$t('pages.email.create_message_group.save_message')"
421
+ color="primary"
422
+ :loading="saving || isTranslating"
423
+ :data-testid="templateEmailCreateTestIds.saveTemplateButton"
424
+ />
425
+ </template>
426
+ </SNavigationBarHeader>
427
+ </template>
428
+
429
+ <div v-if="validationErrors.length">
430
+ <UAlert
431
+ color="error"
432
+ variant="subtle"
433
+ icon="i-lucide-alert-triangle"
434
+ :title="$t('editor.validation.error.title')"
435
+ :close="true"
436
+ @update:open="clearErrors()"
437
+ :data-testid="templateEmailCreateTestIds.validationAlert"
438
+ >
439
+ <template #description>
440
+ <ul class="list-disc pl-4 space-y-1">
441
+ <li v-for="err in validationErrors" :key="err">
442
+ {{ err }}
443
+ </li>
444
+ </ul>
445
+ </template>
446
+ </UAlert>
447
+ </div>
448
+
449
+ <UForm
450
+ ref="templateForm"
451
+ :schema="draftSchema"
452
+ :state="formState"
453
+ :validate-on="[]"
454
+ class="flex-1 relative min-h-0 flex flex-col"
455
+ >
456
+ <UCard
457
+ v-if="activeTab === 'content'"
458
+ :ui="{
459
+ root: 'flex flex-col h-full',
460
+ body: 'flex-1 overflow-hidden sm:px-0 px-0 py-0 sm:py-0',
461
+ header: 'sm:px-4 px-4 py-4 sm:py-4',
462
+ }"
463
+ :data-testid="templateEmailCreateTestIds.contentCard"
464
+ >
465
+ <template #header>
466
+ <div class="grid grid-cols-3 gap-2 items-center">
467
+ <UFieldGroup>
468
+ <UBadge variant="outline" color="neutral" size="lg">{{
469
+ $t('pages.email.create_message_group.name')
470
+ }}</UBadge>
471
+ <UInput
472
+ v-model="name"
473
+ :placeholder="$t('pages.email.create_message_group.name_placeholder')"
474
+ :data-testid="templateEmailCreateTestIds.nameInput"
475
+ />
476
+ </UFieldGroup>
477
+
478
+ <UFieldGroup v-if="messages[language]">
479
+ <UBadge variant="outline" color="neutral" size="lg">{{
480
+ $t('pages.email.create_message_group.subject')
481
+ }}</UBadge>
482
+ <UInput
483
+ v-model="messages[language]!.body.subject"
484
+ :placeholder="$t('pages.email.create_message_group.subject_placeholder')"
485
+ :data-testid="templateEmailCreateTestIds.subjectInput"
486
+ />
487
+ </UFieldGroup>
488
+ <USwitch
489
+ :label="$t('common.internal')"
490
+ v-if="adminStore.isAdminModeActive"
491
+ v-model="isInternal"
492
+ color="info"
493
+ :data-testid="templateEmailCreateTestIds.internalSwitch"
494
+ />
495
+ </div>
496
+ </template>
497
+
498
+ <div class="flex flex-col h-full overflow-hidden">
499
+ <template v-if="messages[language]" :key="language">
500
+ <div class="flex-1 overflow-hidden">
501
+ <EmailEditor
502
+ :message="messages[language]!"
503
+ @block-added="onBlockAdded"
504
+ @block-deleted="onBlockDeleted"
505
+ @block-duplicated="onBlockDuplicated"
506
+ @block-sorted="onBlockSorted"
507
+ @widget-tab-changed="onWidgetTabChanged"
508
+ @test-sent="onTestSent"
509
+ />
510
+ </div>
511
+ </template>
512
+ </div>
513
+ </UCard>
514
+
515
+ <UCard
516
+ v-if="activeTab === 'translations' && messages[language]"
517
+ :ui="{
518
+ root: 'flex flex-col h-full',
519
+ body: 'flex-1 overflow-hidden sm:px-0 px-0 py-0 sm:py-0',
520
+ header: 'sm:px-4 px-4 py-4 sm:py-4',
521
+ }"
522
+ :data-testid="templateEmailCreateTestIds.translationsCard"
523
+ >
524
+ <div
525
+ class="grid grid-cols-[auto_1fr] divide-x divide-muted overflow-auto max-h-full h-full"
526
+ >
527
+ <div class="overflow-y-auto">
528
+ <LanguageList
529
+ v-model:messages="messages"
530
+ v-model:language="targetLanguage"
531
+ render-type="email_v1"
532
+ :main-language-id="language"
533
+ @add-language="onAddLanguage"
534
+ @remove-language="onRemoveLanguage"
535
+ />
536
+ </div>
537
+ <div class="overflow-y-auto" v-if="messages[language] && messages[targetLanguage]">
538
+ <TranslationEditor
539
+ :base-message="messages[language]!"
540
+ v-model="messages[targetLanguage]!"
541
+ :messages="messages"
542
+ :current-language="targetLanguage"
543
+ :base-language="language"
544
+ :key="targetLanguage"
545
+ @auto-translate-tracked="onAutoTranslateTracked"
546
+ @preview="onPreview"
547
+ @filters-changed="onFiltersChanged"
548
+ />
549
+ </div>
550
+ </div>
551
+ </UCard>
552
+ </UForm>
553
+
554
+ <IncompleteTranslationsDialog
555
+ v-model="showIncompleteDialog"
556
+ :incomplete-languages="incompleteLanguages"
557
+ @save-without-translating="proceedWithSave"
558
+ @translate-and-save="translateAndSave"
559
+ @go-to-translations="goToTranslations"
560
+ />
561
+ </SNavigationPage>
562
+ </template>