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