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