@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.
@@ -5,360 +5,9 @@ definePageMeta({
5
5
  channel: 'whatsapp',
6
6
  })
7
7
 
8
- import WhatsappEditorSyncWrapper from '@/components/Whatsapp/Editor/SyncWrapper.vue'
9
- import WhatsappPreview from '@/components/Whatsapp/PreviewPanel.vue'
10
- import TranslationEditor from '@/components/Whatsapp/TranslationEditor.vue'
11
- import LanguageList from '@/components/Common/LanguageList.vue'
12
- import IncompleteTranslationsDialog from '@/components/Common/IncompleteTranslationsDialog.vue'
13
-
14
- import CategoryChanger from '@/components/Whatsapp/CategoryChanger.vue'
15
- import { computed, ref } from 'vue'
16
- import {
17
- type MessageFromDb,
18
- type MessageGroupWithMessages,
19
- type MessageGroupCategory,
20
- type WhatsappMessage,
21
- type LanguageFromDb,
22
- type PostEntity,
23
- } from '@dev.smartpricing/message-composer-utils/types'
24
- import { useRoute } from 'vue-router'
25
- import { useMessageGroupQuery, useLanguagesQuery } from '~/queries'
26
- import { useMetaSubmissionStore } from '@/stores/metaSubmission'
27
- import { useAppContextStore } from '@/stores/appContext'
28
- import type { WaHeaderType } from '~/types/tracking'
29
-
30
- const { trackEvent } = useTracking()
31
8
  const route = useRoute()
32
-
33
- const { mainLanguage } = storeToRefs(useAppContextStore())
34
-
35
- const language = ref(mainLanguage.value)
36
- const { goBackOrFallback } = useGoBack()
37
-
38
- const activeTab = ref<'content' | 'translations'>('content')
39
- const targetLanguage = ref(mainLanguage.value)
40
-
41
- const messages = ref<Record<string, WhatsappMessage>>({})
42
- const { data: messageGroupData, isPending: isFetching } = useMessageGroupQuery(
43
- route.params.id as string,
44
- )
45
-
46
- // Create a local editable copy to avoid mutating cache
47
- const messageGroup = ref<MessageGroupWithMessages | null>(null)
48
-
49
- // Initialize change tracking with messageGroupId
50
- const messageGroupId = computed(() => route.params.id as string)
51
- const changeTracking = useChangeTracking(messageGroupId)
52
-
53
- // Store original messageGroup values
54
- const originalMessageGroupName = ref<string>('')
55
- const originalMessageGroupCategory = ref<MessageGroupCategory>('marketing')
56
-
57
- // Watch for data changes to process messages
58
- watch(
59
- messageGroupData,
60
- (newValue) => {
61
- if (!newValue?.messages) return
62
-
63
- // Deep clone to avoid mutating cache
64
- messageGroup.value = structuredClone(newValue)
65
-
66
- // Store original messageGroup metadata
67
- originalMessageGroupName.value = newValue.name
68
- originalMessageGroupCategory.value = newValue.category
69
-
70
- for (const message of newValue.messages) {
71
- if (message.render_type === 'whatsapp_v1') {
72
- // Clone message to avoid cache mutation
73
- messages.value[message.language_id] = structuredClone(message) as WhatsappMessage
74
- // Record original state for change tracking
75
- changeTracking.recordOriginalState(message.language_id, message as WhatsappMessage)
76
- }
77
- }
78
- const resolved = resolveDefaultLanguage(messages.value, mainLanguage.value)
79
- if (resolved && resolved !== mainLanguage.value) {
80
- language.value = resolved
81
- targetLanguage.value = resolved
82
- }
83
- },
84
- { immediate: true },
85
- )
86
-
87
- const { formState, draftSchema, validationErrors, validateDraft, validateFull, clearErrors } =
88
- useTemplateFormValidation({
89
- type: 'whatsapp',
90
- name: computed(() => messageGroup.value?.name ?? ''),
91
- category: computed(() => messageGroup.value?.category ?? 'marketing'),
92
- messages,
93
- })
94
-
95
- const { saving, createTemplate, updateTemplate } = useTemplateOperations({
96
- onSuccess: () => {
97
- saved.value = true
98
- changeTracking.clearAllChanges()
99
- },
100
- })
101
-
102
- const { submitToMeta } = useMetaSubmissionStore()
103
-
104
- // Create computed property for category to watch changes
105
- const currentCategory = computed(() => messageGroup.value?.category)
106
-
107
- // Initialize WhatsApp template behaviors composable
108
- const { ensureUnsubscribeButton } = useWhatsappTemplateBehaviors({
109
- category: currentCategory,
110
- messages,
111
- })
112
-
113
- const saved = ref(false)
114
-
115
- const hasAnyChanges = computed(() => {
116
- // Check if message group name or category changed
117
- if (messageGroup.value) {
118
- const nameChanged = messageGroup.value.name !== originalMessageGroupName.value
119
- const categoryChanged = messageGroup.value.category !== originalMessageGroupCategory.value
120
- if (nameChanged || categoryChanged) return true
121
- }
122
-
123
- // Check if any language has changes
124
- for (const languageId in messages.value) {
125
- const message = messages.value[languageId]
126
- if (message && changeTracking.hasChanges(languageId, message as WhatsappMessage)) {
127
- return true
128
- }
129
- }
130
- return false
131
- })
132
-
133
- useExitConfirmation(computed(() => hasAnyChanges.value && !saved.value))
134
-
135
- // ── Tracking helpers ──
136
- function getWaBaseProps() {
137
- return { name: messageGroup.value?.name ?? '', category: messageGroup.value?.category ?? '' }
138
- }
139
-
140
- function getWaDraftProps() {
141
- const resolved = resolveDefaultLanguage(messages.value, mainLanguage.value)
142
- const msg = resolved ? messages.value[resolved] : undefined
143
- return {
144
- ...getWaBaseProps(),
145
- languages: Object.keys(messages.value),
146
- header: (msg?.body?.header?.type ?? 'none') as WaHeaderType,
147
- footer: !!msg?.body?.footer?.enabled,
148
- buttons: msg?.body?.buttons?.buttons?.length ?? 0,
149
- is_internal: messageGroup.value?.visibility === 'internal',
150
- }
151
- }
152
-
153
- watch(activeTab, (tab) => {
154
- trackEvent('wa_edit.change_tab', { ...getWaBaseProps(), tab })
155
- })
156
-
157
- function onAddLanguage(lang: string) {
158
- trackEvent('wa_edit.add_language', { ...getWaBaseProps(), language: lang })
159
- }
160
-
161
- function onRemoveLanguage(lang: string) {
162
- trackEvent('wa_edit.remove_language', {
163
- ...getWaBaseProps(),
164
- language: lang,
165
- })
166
- }
167
-
168
- function onAutoTranslateTracked(lang: string) {
169
- trackEvent('wa_edit.auto_translate', {
170
- ...getWaBaseProps(),
171
- language: lang,
172
- })
173
- }
174
-
175
- function onPreview(lang: string) {
176
- trackEvent('wa_edit.preview', { ...getWaBaseProps(), language: lang })
177
- }
178
-
179
- function onFiltersChanged(payload: {
180
- language: string
181
- search_term: string
182
- show_only_missing: boolean
183
- show_only_changes: boolean
184
- }) {
185
- trackEvent('wa_edit.translations_filters', { ...getWaBaseProps(), ...payload })
186
- }
187
-
188
- // Get languages for incomplete translations check
189
- const { data: languages } = useLanguagesQuery()
190
-
191
- // Check for incomplete translations
192
- const { incompleteLanguages, hasIncompleteTranslations } = useIncompleteTranslations({
193
- messages,
194
- languages,
195
- })
196
-
197
- // Dialog state and pending action
198
- const showIncompleteDialog = ref(false)
199
- const pendingAction = ref<'save_draft' | 'save_as_new' | 'request_approval' | null>(null)
200
-
201
- // Auto-translate composable
202
- const { translateMessage, isTranslating } = useAutoTranslate()
203
-
204
- async function editTemplate() {
205
- if (!messageGroup.value) return
206
-
207
- const templateData = {
208
- messageGroup: {
209
- name: messageGroup.value.name,
210
- category: messageGroup.value.category,
211
- },
212
- messages: messages.value,
213
- }
214
- await updateTemplate(route.params.id as string, templateData)
215
- notifyParent('TEMPLATE_SAVED_DRAFT', { id: messageGroup.value.id, name: messageGroup.value.name })
216
- goBackOrFallback()
217
- }
218
-
219
- async function saveAsNew() {
220
- if (!messageGroup.value) return
221
- trackEvent('wa_edit.save_as_new', getWaDraftProps())
222
-
223
- const cleanedMessages: Record<string, PostEntity<WhatsappMessage>> = {}
224
- for (const [languageId, message] of Object.entries(messages.value)) {
225
- const { id, message_group_id, created_at, updated_at, ...templateMessage } = message
226
- cleanedMessages[languageId] = {
227
- ...templateMessage,
228
- message_group_id: '',
229
- metadata: {},
230
- } as PostEntity<WhatsappMessage>
231
- }
232
-
233
- const templateData = {
234
- messageGroup: {
235
- name: messageGroup.value.name,
236
- category: messageGroup.value.category,
237
- },
238
- messages: cleanedMessages,
239
- }
240
- const result = await createTemplate(templateData, 'draft')
241
- if (result) {
242
- notifyParent('TEMPLATE_SAVED_AS_NEW', { id: result.id, name: result.name })
243
- navigateTo({ name: 'whatsapp-message-edit', params: { id: result.id } }, { replace: true })
244
- }
245
- }
246
-
247
- async function requestApproval() {
248
- if (!messageGroup.value) return
249
- trackEvent('wa_edit.request_approval', getWaDraftProps())
250
-
251
- const templateData = {
252
- messageGroup: {
253
- name: messageGroup.value.name,
254
- category: messageGroup.value.category,
255
- },
256
- messages: messages.value,
257
- }
258
- await updateTemplate(route.params.id as string, templateData)
259
- await submitToMeta(route.params.id as string)
260
- notifyParent('TEMPLATE_APPROVAL_REQUESTED', {
261
- id: messageGroup.value.id,
262
- name: messageGroup.value.name,
263
- })
264
- goBackOrFallback()
265
- }
266
-
267
- // Wrapper functions to validate then check for incomplete translations
268
- async function handleSaveDraft() {
269
- if (!(await validateDraft())) return
270
- if (hasIncompleteTranslations.value) {
271
- pendingAction.value = 'save_draft'
272
- showIncompleteDialog.value = true
273
- } else {
274
- editTemplate()
275
- }
276
- }
277
-
278
- async function handleSaveAsNew() {
279
- if (!(await validateDraft())) return
280
- if (hasIncompleteTranslations.value) {
281
- pendingAction.value = 'save_as_new'
282
- showIncompleteDialog.value = true
283
- } else {
284
- saveAsNew()
285
- }
286
- }
287
-
288
- async function handleRequestApproval() {
289
- if (!(await validateFull())) return
290
- if (hasIncompleteTranslations.value) {
291
- pendingAction.value = 'request_approval'
292
- showIncompleteDialog.value = true
293
- } else {
294
- requestApproval()
295
- }
296
- }
297
-
298
- // Proceed with save without translating
299
- function proceedWithSave() {
300
- if (pendingAction.value === 'save_draft') {
301
- editTemplate()
302
- } else if (pendingAction.value === 'save_as_new') {
303
- saveAsNew()
304
- } else if (pendingAction.value === 'request_approval') {
305
- requestApproval()
306
- }
307
- pendingAction.value = null
308
- }
309
-
310
- // Go to translations tab
311
- function goToTranslations() {
312
- activeTab.value = 'translations'
313
- pendingAction.value = null
314
- }
315
-
316
- // Translate automatically and save
317
- async function translateAndSave() {
318
- if (!incompleteLanguages.value.length) {
319
- proceedWithSave()
320
- return
321
- }
322
-
323
- const baseMessage = messages.value[language.value]
324
- if (!baseMessage) {
325
- proceedWithSave()
326
- return
327
- }
328
-
329
- // Translate all incomplete languages
330
- for (const { language: lang } of incompleteLanguages.value) {
331
- const targetMessage = messages.value[lang.id]
332
- if (!targetMessage) continue
333
-
334
- await translateMessage(
335
- baseMessage as WhatsappMessage,
336
- targetMessage as WhatsappMessage,
337
- language.value,
338
- lang.id,
339
- )
340
- }
341
-
342
- // After translation, proceed with save
343
- proceedWithSave()
344
- }
345
- const isPending = computed(() =>
346
- messageGroup.value?.messages?.some((message: MessageFromDb) => message.status === 'pending'),
347
- )
348
- const isApproved = computed(() =>
349
- messageGroup.value?.messages?.some((message: MessageFromDb) => message.status === 'approved'),
350
- )
351
-
352
- watch(
353
- () => messages.value[mainLanguage.value],
354
- () => {
355
- const resolved = resolveDefaultLanguage(messages.value, mainLanguage.value)
356
- if (resolved && resolved !== mainLanguage.value) {
357
- language.value = resolved
358
- targetLanguage.value = resolved
359
- }
360
- },
361
- )
9
+ const composerRef = ref<InstanceType<typeof ComposerWhatsappComposer>>()
10
+ const store = useWhatsappComposerStore()
362
11
  </script>
363
12
 
364
13
  <template>
@@ -368,192 +17,55 @@ watch(
368
17
  { label: $t('pages.whatsapp.tabs.content'), value: 'content' },
369
18
  { label: $t('pages.whatsapp.tabs.translations'), value: 'translations' },
370
19
  ]"
371
- :active-tab="activeTab"
20
+ :active-tab="store.activeTab"
372
21
  :test-id="templateWhatsappEditTestIds.navbar"
373
- @tab-change="activeTab = $event"
22
+ @tab-change="store.activeTab = $event as 'content' | 'translations'"
374
23
  >
375
24
  <template #actions>
376
- <template v-if="messageGroup">
25
+ <template v-if="store.messageGroup">
377
26
  <UButton
378
- @click="
379
- () => {
380
- notifyParent('TEMPLATE_CANCELLED', { id: messageGroup?.id })
381
- goBackOrFallback()
382
- }
383
- "
27
+ @click="composerRef?.handleCancel()"
384
28
  :label="$t('common.actions.cancel')"
385
29
  color="primary"
386
30
  variant="ghost"
387
- :loading="saving || isTranslating"
31
+ :loading="composerRef?.saving || composerRef?.isTranslating"
388
32
  :data-testid="templateWhatsappEditTestIds.cancelButton"
389
33
  />
390
34
  <UButton
391
- v-if="isPending || isApproved"
392
- @click="handleSaveAsNew()"
35
+ v-if="store.isPending || store.isApproved"
36
+ @click="composerRef?.handleSaveAsNew()"
393
37
  :label="$t('pages.whatsapp.create_message_group.save_as_new')"
394
38
  color="secondary"
395
39
  variant="outline"
396
- :loading="saving || isTranslating"
40
+ :loading="composerRef?.saving || composerRef?.isTranslating"
397
41
  :data-testid="templateWhatsappEditTestIds.saveAsNewButton"
398
42
  />
399
43
  <UButton
400
44
  v-else
401
- @click="handleSaveDraft()"
45
+ @click="composerRef?.handleSaveDraft()"
402
46
  :label="$t('pages.whatsapp.create_message_group.save_draft')"
403
47
  color="secondary"
404
48
  variant="outline"
405
- :loading="saving || isTranslating"
49
+ :loading="composerRef?.saving || composerRef?.isTranslating"
406
50
  :data-testid="templateWhatsappEditTestIds.saveDraftButton"
407
51
  />
408
52
  <UButton
409
- v-if="!isPending && !isApproved"
410
- @click="handleRequestApproval()"
53
+ v-if="!store.isPending && !store.isApproved"
54
+ @click="composerRef?.handleRequestApproval()"
411
55
  :label="$t('pages.whatsapp.create_message_group.request_approval')"
412
56
  color="primary"
413
- :loading="saving || isTranslating"
57
+ :loading="composerRef?.saving || composerRef?.isTranslating"
414
58
  :data-testid="templateWhatsappEditTestIds.requestApprovalButton"
415
59
  />
416
60
  </template>
417
61
  </template>
418
62
 
419
- <div v-if="validationErrors.length">
420
- <UAlert
421
- color="error"
422
- variant="subtle"
423
- icon="i-lucide-alert-triangle"
424
- :title="$t('editor.validation.error.title')"
425
- :close="true"
426
- @update:open="clearErrors()"
427
- :data-testid="templateWhatsappEditTestIds.validationAlert"
428
- >
429
- <template #description>
430
- <ul class="list-disc pl-4 space-y-1">
431
- <li v-for="err in validationErrors" :key="err">
432
- {{ err }}
433
- </li>
434
- </ul>
435
- </template>
436
- </UAlert>
437
- </div>
438
-
439
- <UForm
440
- ref="templateForm"
441
- :schema="draftSchema"
442
- :state="formState"
443
- :validate-on="[]"
444
- class="flex-1 relative min-h-0 flex flex-col"
63
+ <ComposerWhatsappComposer
64
+ ref="composerRef"
65
+ mode="edit"
66
+ :message-group-id="route.params.id as string"
445
67
  >
446
- <UCard
447
- v-if="activeTab === 'content' && messageGroup"
448
- :ui="{
449
- root: 'flex flex-col',
450
- body: 'flex-1 overflow-hidden sm:px-0 px-0 py-0 sm:py-0',
451
- header: 'sm:px-4 px-4 py-4 sm:py-4',
452
- }"
453
- :data-testid="templateWhatsappEditTestIds.contentCard"
454
- >
455
- <template #header>
456
- <div class="grid grid-cols-3 gap-2">
457
- <UFieldGroup>
458
- <UBadge variant="outline" color="neutral" size="lg">{{
459
- $t('pages.whatsapp.edit_message_group.name')
460
- }}</UBadge>
461
- <UInput
462
- v-model="messageGroup.name"
463
- :placeholder="$t('pages.whatsapp.edit_message_group.name_placeholder')"
464
- :data-testid="templateWhatsappEditTestIds.nameInput"
465
- />
466
- </UFieldGroup>
467
-
468
- <UFieldGroup>
469
- <UBadge variant="outline" color="neutral" size="lg">{{
470
- $t('pages.whatsapp.edit_message_group.category')
471
- }}</UBadge>
472
- <CategoryChanger
473
- v-model:category="messageGroup.category"
474
- @changed="ensureUnsubscribeButton"
475
- />
476
- </UFieldGroup>
477
- <!-- <DocumentationPanel
478
- :category="$t('documentation.whatsapp_category.category')"
479
- :title="$t('documentation.whatsapp_category.title')"
480
- :description="$t('documentation.whatsapp_category.description')"
481
- :info="{
482
- title: '',
483
- text: $t('documentation.whatsapp_category.info'),
484
- }"
485
- :content="[
486
- $t('documentation.whatsapp_category.content.marketing'),
487
- $t('documentation.whatsapp_category.content.utility'),
488
- ]"
489
- /> -->
490
- </div>
491
- </template>
492
-
493
- <div class="grid grid-cols-3 divide-x divide-muted overflow-auto max-h-full h-full">
494
- <div class="col-span-2 overflow-y-auto pt-4 pb-4" v-if="messages[language]">
495
- <h2 class="px-4 pb-4 label-lg">
496
- {{ $t('pages.whatsapp.edit_message_group.message_content') }}
497
- </h2>
498
- <WhatsappEditorSyncWrapper
499
- v-model="messages[language]"
500
- :messages="messages"
501
- :main-language-id="language"
502
- :key="language"
503
- />
504
- </div>
505
- <div class="col-span-1 overflow-y-auto" v-if="messages[language]">
506
- <WhatsappPreview :message="messages[language]" />
507
- </div>
508
- </div>
509
- </UCard>
510
-
511
- <UCard
512
- v-if="activeTab === 'translations' && messageGroup"
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="templateWhatsappEditTestIds.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="whatsapp_v1"
528
- :message-group-id="messageGroupId"
529
- :main-language-id="language"
530
- @add-language="onAddLanguage"
531
- @remove-language="onRemoveLanguage"
532
- />
533
- </div>
534
- <div class="overflow-y-auto" v-if="messages[language] && messages[targetLanguage]">
535
- <TranslationEditor
536
- :base-message="messages[language] || messages['en']"
537
- v-model:target-message="messages[targetLanguage]"
538
- :messages="messages"
539
- :current-language="targetLanguage"
540
- :base-language="language"
541
- :message-group-id="messageGroupId"
542
- @auto-translate-tracked="onAutoTranslateTracked"
543
- @preview="onPreview"
544
- @filters-changed="onFiltersChanged"
545
- />
546
- </div>
547
- </div>
548
- </UCard>
549
- </UForm>
550
-
551
- <IncompleteTranslationsDialog
552
- v-model="showIncompleteDialog"
553
- :incomplete-languages="incompleteLanguages"
554
- @save-without-translating="proceedWithSave"
555
- @translate-and-save="translateAndSave"
556
- @go-to-translations="goToTranslations"
557
- />
68
+ <template #actions></template>
69
+ </ComposerWhatsappComposer>
558
70
  </LayoutBasePage>
559
71
  </template>