@dev.smartpricing/message-composer-layer 1.0.13 → 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.
- package/app/components/Composer/BrandList.vue +190 -0
- package/app/components/Composer/EmailComposer.vue +501 -0
- package/app/components/Composer/EmailContent.vue +95 -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 +424 -0
- package/app/components/Composer/WhatsappContent.vue +88 -0
- package/app/components/Composer/WhatsappTranslations.vue +68 -0
- 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/package.json +2 -2
|
@@ -5,375 +5,8 @@ definePageMeta({
|
|
|
5
5
|
channel: 'email',
|
|
6
6
|
})
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
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 = ref<InstanceType<typeof ComposerEmailComposer>>()
|
|
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
|
-
<
|
|
428
|
-
<
|
|
429
|
-
|
|
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>
|