@dev.smartpricing/message-composer-layer 1.0.12 → 1.0.14

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.
@@ -1,377 +1,12 @@
1
1
  <script setup lang="ts">
2
2
  definePageMeta({
3
+ name: 'email-message-create',
3
4
  middleware: ['channel-guard'],
5
+ channel: 'email',
4
6
  })
5
7
 
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
- }
8
+ const composerRef = ref<InstanceType<typeof ComposerEmailComposer>>()
9
+ const store = useEmailComposerStore()
375
10
  </script>
376
11
 
377
12
  <template>
@@ -387,172 +22,38 @@ async function translateAndSave() {
387
22
  value: 'translations',
388
23
  },
389
24
  ]"
390
- :active-tab="activeTab"
25
+ :active-tab="store.activeTab"
391
26
  :test-id="templateEmailCreateTestIds.navbar"
392
- @tab-change="activeTab = $event"
27
+ @tab-change="store.activeTab = $event as 'content' | 'translations'"
393
28
  >
394
29
  <template #actions>
395
30
  <UButton
396
- @click="
397
- () => {
398
- notifyParent('TEMPLATE_CANCELLED')
399
- goBackOrFallback()
400
- }
401
- "
31
+ @click="composerRef?.handleCancel()"
402
32
  :label="$t('common.actions.cancel')"
403
33
  color="primary"
404
34
  variant="ghost"
405
- :loading="saving || isTranslating"
35
+ :loading="composerRef?.saving || composerRef?.isTranslating"
406
36
  :data-testid="templateEmailCreateTestIds.cancelButton"
407
37
  />
408
38
  <UButton
409
- @click="handleSaveAsDraft"
39
+ @click="composerRef?.handleSaveAsDraft()"
410
40
  :label="$t('pages.email.create_message_group.save_as_draft')"
411
41
  color="primary"
412
42
  variant="outline"
413
- :loading="saving || isTranslating"
43
+ :loading="composerRef?.saving || composerRef?.isTranslating"
414
44
  :data-testid="templateEmailCreateTestIds.saveDraftButton"
415
45
  />
416
46
  <UButton
417
- @click="handleSaveTemplate"
47
+ @click="composerRef?.handleSaveTemplate()"
418
48
  :label="$t('pages.email.create_message_group.save_message')"
419
49
  color="primary"
420
- :loading="saving || isTranslating"
50
+ :loading="composerRef?.saving || composerRef?.isTranslating"
421
51
  :data-testid="templateEmailCreateTestIds.saveTemplateButton"
422
52
  />
423
53
  </template>
424
54
 
425
- <div v-if="validationErrors.length">
426
- <UAlert
427
- color="error"
428
- variant="subtle"
429
- icon="i-lucide-alert-triangle"
430
- :title="$t('editor.validation.error.title')"
431
- :close="true"
432
- @update:open="clearErrors()"
433
- :data-testid="templateEmailCreateTestIds.validationAlert"
434
- >
435
- <template #description>
436
- <ul class="list-disc pl-4 space-y-1">
437
- <li v-for="err in validationErrors" :key="err">
438
- {{ err }}
439
- </li>
440
- </ul>
441
- </template>
442
- </UAlert>
443
- </div>
444
-
445
- <UForm
446
- ref="templateForm"
447
- :schema="draftSchema"
448
- :state="formState"
449
- :validate-on="[]"
450
- class="flex-1 relative min-h-0 flex flex-col"
451
- >
452
- <UCard
453
- v-if="activeTab === 'content'"
454
- :ui="{
455
- root: 'flex flex-col h-full',
456
- body: 'flex-1 overflow-hidden sm:px-0 px-0 py-0 sm:py-0',
457
- header: 'sm:px-4 px-4 py-4 sm:py-4',
458
- }"
459
- :data-testid="templateEmailCreateTestIds.contentCard"
460
- >
461
- <template #header>
462
- <div class="grid grid-cols-3 gap-2 items-center">
463
- <UFieldGroup>
464
- <UBadge variant="outline" color="neutral" size="lg">{{
465
- $t('pages.email.create_message_group.name')
466
- }}</UBadge>
467
- <UInput
468
- v-model="name"
469
- :placeholder="$t('pages.email.create_message_group.name_placeholder')"
470
- :data-testid="templateEmailCreateTestIds.nameInput"
471
- />
472
- </UFieldGroup>
473
-
474
- <UFieldGroup v-if="messages[language]">
475
- <UBadge variant="outline" color="neutral" size="lg">{{
476
- $t('pages.email.create_message_group.subject')
477
- }}</UBadge>
478
- <UInput
479
- v-model="messages[language]!.body.subject"
480
- :placeholder="$t('pages.email.create_message_group.subject_placeholder')"
481
- :data-testid="templateEmailCreateTestIds.subjectInput"
482
- />
483
- </UFieldGroup>
484
- <USwitch
485
- :label="$t('common.internal')"
486
- v-if="adminStore.isAdminModeActive"
487
- v-model="isInternal"
488
- color="info"
489
- :data-testid="templateEmailCreateTestIds.internalSwitch"
490
- />
491
- </div>
492
- </template>
493
-
494
- <div class="flex flex-col h-full overflow-hidden">
495
- <template v-if="messages[language]" :key="language">
496
- <div class="flex-1 overflow-hidden">
497
- <EmailEditor
498
- :message="messages[language]!"
499
- @block-added="onBlockAdded"
500
- @block-deleted="onBlockDeleted"
501
- @block-duplicated="onBlockDuplicated"
502
- @block-sorted="onBlockSorted"
503
- @widget-tab-changed="onWidgetTabChanged"
504
- @test-sent="onTestSent"
505
- />
506
- </div>
507
- </template>
508
- </div>
509
- </UCard>
510
-
511
- <UCard
512
- v-if="activeTab === 'translations' && messages[language]"
513
- :ui="{
514
- root: 'flex flex-col h-full',
515
- body: 'flex-1 overflow-hidden sm:px-0 px-0 py-0 sm:py-0',
516
- header: 'sm:px-4 px-4 py-4 sm:py-4',
517
- }"
518
- :data-testid="templateEmailCreateTestIds.translationsCard"
519
- >
520
- <div
521
- class="grid grid-cols-[auto_1fr] divide-x divide-muted overflow-auto max-h-full h-full"
522
- >
523
- <div class="overflow-y-auto">
524
- <LanguageList
525
- v-model:messages="messages"
526
- v-model:language="targetLanguage"
527
- render-type="email_v1"
528
- :main-language-id="language"
529
- @add-language="onAddLanguage"
530
- @remove-language="onRemoveLanguage"
531
- />
532
- </div>
533
- <div class="overflow-y-auto" v-if="messages[language] && messages[targetLanguage]">
534
- <TranslationEditor
535
- :base-message="messages[language]!"
536
- v-model="messages[targetLanguage]!"
537
- :messages="messages"
538
- :current-language="targetLanguage"
539
- :base-language="language"
540
- :key="targetLanguage"
541
- @auto-translate-tracked="onAutoTranslateTracked"
542
- @preview="onPreview"
543
- @filters-changed="onFiltersChanged"
544
- />
545
- </div>
546
- </div>
547
- </UCard>
548
- </UForm>
549
-
550
- <IncompleteTranslationsDialog
551
- v-model="showIncompleteDialog"
552
- :incomplete-languages="incompleteLanguages"
553
- @save-without-translating="proceedWithSave"
554
- @translate-and-save="translateAndSave"
555
- @go-to-translations="goToTranslations"
556
- />
55
+ <ComposerEmailComposer ref="composerRef" mode="create">
56
+ <template #actions></template>
57
+ </ComposerEmailComposer>
557
58
  </LayoutBasePage>
558
59
  </template>