@dev.smartpricing/message-composer-layer 1.0.13 → 1.0.15

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