@dev.smartpricing/message-composer-layer 4.0.1 → 4.0.2-templates.1

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.
@@ -0,0 +1,69 @@
1
+ <script setup lang="ts">
2
+ import type { TemplateWithMessageSummary } from '@dev.smartpricing/message-composer-utils/types'
3
+
4
+ const props = defineProps<{
5
+ template: TemplateWithMessageSummary | null
6
+ }>()
7
+
8
+ const emit = defineEmits<{
9
+ confirm: [mode: 'clone' | 'convert']
10
+ }>()
11
+
12
+ const open = defineModel<boolean>('open', { required: true })
13
+
14
+ const { t } = useI18n()
15
+ const { mutateAsync: promoteTemplate, isPending } = usePromoteTemplateMutation()
16
+ const toast = useToast()
17
+
18
+ const mode = ref<'clone' | 'convert'>('clone')
19
+
20
+ const items = computed(() => [
21
+ {
22
+ label: t('templates.dialogs.promote.clone_label'),
23
+ description: t('templates.dialogs.promote.clone_description'),
24
+ value: 'clone' as const,
25
+ },
26
+ {
27
+ label: t('templates.dialogs.promote.convert_label'),
28
+ description: t('templates.dialogs.promote.convert_description'),
29
+ value: 'convert' as const,
30
+ },
31
+ ])
32
+
33
+ async function handleConfirm() {
34
+ if (!props.template) return
35
+ await promoteTemplate({ id: props.template.id, data: { mode: mode.value } })
36
+ const key = mode.value === 'clone' ? 'success_clone' : 'success_convert'
37
+ toast.add({
38
+ title: t(`templates.dialogs.promote.${key}`, { name: props.template.name }),
39
+ color: 'success',
40
+ })
41
+ open.value = false
42
+ }
43
+
44
+ watch(open, (v) => {
45
+ if (v) mode.value = 'clone'
46
+ })
47
+ </script>
48
+
49
+ <template>
50
+ <UModal
51
+ v-model:open="open"
52
+ :title="$t('templates.dialogs.promote.title')"
53
+ :ui="{ footer: 'justify-end' }"
54
+ >
55
+ <template #body>
56
+ <URadioGroup v-model="mode" :items="items" variant="card" color="secondary" />
57
+ </template>
58
+
59
+ <template #footer>
60
+ <UButton variant="outline" :label="$t('common.actions.cancel')" @click="open = false" />
61
+ <UButton
62
+ color="primary"
63
+ :label="$t('common.actions.confirm')"
64
+ :loading="isPending"
65
+ @click="handleConfirm"
66
+ />
67
+ </template>
68
+ </UModal>
69
+ </template>
@@ -5,8 +5,12 @@ definePageMeta({
5
5
  channel: 'email',
6
6
  })
7
7
 
8
+ const route = useRoute()
8
9
  const composerRef = useTemplateRef('composerRef')
9
10
  const store = useEmailComposerStore()
11
+
12
+ const fromTemplateId = computed(() => route.query.from_template as string | undefined)
13
+ const brandId = computed(() => route.query.brand_id as string | undefined)
10
14
  </script>
11
15
 
12
16
  <template>
@@ -52,7 +56,12 @@ const store = useEmailComposerStore()
52
56
  />
53
57
  </template>
54
58
 
55
- <ComposerEmailComposer ref="composerRef" mode="create">
59
+ <ComposerEmailComposer
60
+ ref="composerRef"
61
+ mode="create"
62
+ :from-template-id="fromTemplateId"
63
+ :brand-id="brandId"
64
+ >
56
65
  <template #actions></template>
57
66
  </ComposerEmailComposer>
58
67
  </LayoutBasePage>
@@ -5,8 +5,11 @@ definePageMeta({
5
5
  channel: 'whatsapp',
6
6
  })
7
7
 
8
+ const route = useRoute()
8
9
  const composerRef = useTemplateRef('composerRef')
9
10
  const store = useWhatsappComposerStore()
11
+
12
+ const fromTemplateId = computed(() => route.query.from_template as string | undefined)
10
13
  </script>
11
14
 
12
15
  <template>
@@ -46,7 +49,7 @@ const store = useWhatsappComposerStore()
46
49
  />
47
50
  </template>
48
51
 
49
- <ComposerWhatsappComposer ref="composerRef" mode="create">
52
+ <ComposerWhatsappComposer ref="composerRef" mode="create" :from-template-id="fromTemplateId">
50
53
  <template #actions></template>
51
54
  </ComposerWhatsappComposer>
52
55
  </LayoutBasePage>
@@ -0,0 +1,82 @@
1
+ <script setup lang="ts">
2
+ import type { EmailMessage, PostEntity } from '@dev.smartpricing/message-composer-utils/types'
3
+
4
+ definePageMeta({
5
+ name: 'email-template-edit',
6
+ middleware: ['channel-guard'],
7
+ channel: 'email',
8
+ })
9
+
10
+ const route = useRoute()
11
+ const composerRef = useTemplateRef('composerRef')
12
+ const store = useEmailComposerStore()
13
+ const toast = useToast()
14
+ const { t } = useI18n()
15
+ const { goBackOrFallback } = useGoBack()
16
+
17
+ const templateId = computed(() => route.params.id as string)
18
+
19
+ const { mutateAsync: updateTemplate } = useUpdateTemplateMutation()
20
+ const saving = ref(false)
21
+
22
+ async function handleSave() {
23
+ saving.value = true
24
+ try {
25
+ const messages = Object.values(store.messages).map((msg) => {
26
+ const m = msg as PostEntity<EmailMessage>
27
+ return {
28
+ language_id: m.language_id,
29
+ render_type: m.render_type,
30
+ body: m.body,
31
+ metadata: m.metadata || {},
32
+ }
33
+ })
34
+
35
+ await updateTemplate({
36
+ id: templateId.value,
37
+ data: {
38
+ name: store.name,
39
+ category: store.category,
40
+ messages,
41
+ },
42
+ })
43
+
44
+ toast.add({ title: t('templates.dialogs.save_as_template.success'), color: 'success' })
45
+ goBackOrFallback()
46
+ } finally {
47
+ saving.value = false
48
+ }
49
+ }
50
+ </script>
51
+
52
+ <template>
53
+ <LayoutBasePage
54
+ :title="$t('templates.edit_template')"
55
+ :tabs="[
56
+ { label: $t('pages.email.edit_message_group.tab_content'), value: 'content' },
57
+ { label: $t('pages.email.edit_message_group.tab_translations'), value: 'translations' },
58
+ ]"
59
+ :active-tab="store.activeTab"
60
+ @tab-change="store.activeTab = $event as 'content' | 'translations'"
61
+ >
62
+ <template #actions>
63
+ <UButton
64
+ @click="composerRef?.handleCancel()"
65
+ :label="$t('common.actions.cancel')"
66
+ color="primary"
67
+ variant="ghost"
68
+ :loading="saving || composerRef?.isTranslating"
69
+ />
70
+ <UButton
71
+ @click="handleSave()"
72
+ :label="$t('common.actions.save')"
73
+ color="primary"
74
+ :loading="saving || composerRef?.isTranslating"
75
+ />
76
+ </template>
77
+
78
+ <ComposerEmailComposer ref="composerRef" mode="create" :from-template-id="templateId">
79
+ <template #actions></template>
80
+ </ComposerEmailComposer>
81
+ </LayoutBasePage>
82
+ </template>
@@ -0,0 +1,74 @@
1
+ <script setup lang="ts">
2
+ import type { EmailMessage, PostEntity } from '@dev.smartpricing/message-composer-utils/types'
3
+
4
+ definePageMeta({
5
+ name: 'email-template-create',
6
+ middleware: ['channel-guard'],
7
+ channel: 'email',
8
+ })
9
+
10
+ const composerRef = useTemplateRef('composerRef')
11
+ const store = useEmailComposerStore()
12
+ const toast = useToast()
13
+ const { t } = useI18n()
14
+ const { goBackOrFallback } = useGoBack()
15
+
16
+ const { mutateAsync: createTemplate } = useCreateTemplateMutation()
17
+ const saving = ref(false)
18
+
19
+ async function handleSave() {
20
+ saving.value = true
21
+ try {
22
+ const messages = Object.values(store.messages).map((msg) => {
23
+ const m = msg as PostEntity<EmailMessage>
24
+ return {
25
+ language_id: m.language_id,
26
+ render_type: m.render_type,
27
+ body: m.body,
28
+ metadata: m.metadata || {},
29
+ }
30
+ })
31
+
32
+ await createTemplate({
33
+ name: store.name,
34
+ category: store.category,
35
+ messages,
36
+ })
37
+
38
+ toast.add({ title: t('templates.dialogs.save_as_template.success'), color: 'success' })
39
+ goBackOrFallback()
40
+ } finally {
41
+ saving.value = false
42
+ }
43
+ }
44
+ </script>
45
+
46
+ <template>
47
+ <LayoutBasePage
48
+ :title="$t('templates.create_template')"
49
+ :tabs="[
50
+ { label: $t('pages.email.create_message_group.tab_content'), value: 'content' },
51
+ { label: $t('pages.email.create_message_group.tab_translations'), value: 'translations' },
52
+ ]"
53
+ :active-tab="store.activeTab"
54
+ @tab-change="store.activeTab = $event as 'content' | 'translations'"
55
+ >
56
+ <template #actions>
57
+ <UButton
58
+ @click="composerRef?.handleCancel()"
59
+ :label="$t('common.actions.cancel')"
60
+ color="primary"
61
+ variant="ghost"
62
+ :loading="saving || composerRef?.isTranslating"
63
+ />
64
+ <UButton
65
+ @click="handleSave()"
66
+ :label="$t('common.actions.save')"
67
+ color="primary"
68
+ :loading="saving || composerRef?.isTranslating"
69
+ />
70
+ </template>
71
+
72
+ <ComposerEmailComposer ref="composerRef" mode="create" />
73
+ </LayoutBasePage>
74
+ </template>
@@ -0,0 +1,263 @@
1
+ <script setup lang="ts">
2
+ import type { RenderType } from '@dev.smartpricing/message-composer-utils/types'
3
+ import { SYSTEM_USER_ID } from '@dev.smartpricing/message-composer-utils/types'
4
+ import type { TemplateWithMessageSummary } from '@dev.smartpricing/message-composer-utils/types'
5
+ import { formatDate } from '@dev.smartpricing/message-composer-utils/utils'
6
+
7
+ definePageMeta({
8
+ name: 'template-library',
9
+ })
10
+
11
+ const { t, locale } = useI18n()
12
+ const toast = useToast()
13
+ const { hasWhatsapp, hasEmail, defaultRenderType, isRenderTypeAllowed } = useChannels()
14
+ const appContext = useAppContextStore()
15
+
16
+ // Render type with session persistence
17
+ const RENDER_TYPE_KEY = 'template_library_render_type'
18
+ const storedRenderType = sessionStorage.getItem(RENDER_TYPE_KEY) as RenderType | null
19
+ const initialRenderType =
20
+ storedRenderType && isRenderTypeAllowed(storedRenderType)
21
+ ? storedRenderType
22
+ : defaultRenderType.value
23
+ const renderType = ref<RenderType>(initialRenderType)
24
+
25
+ watch(renderType, (newType) => {
26
+ sessionStorage.setItem(RENDER_TYPE_KEY, newType)
27
+ })
28
+
29
+ const scope = ref<'all' | 'mine' | 'system'>('all')
30
+
31
+ const { data: templates, isPending } = useTemplatesQuery(
32
+ computed(() => ({
33
+ renderType: renderType.value,
34
+ scope: scope.value,
35
+ })),
36
+ )
37
+
38
+ const { mutateAsync: deleteTemplate } = useDeleteTemplateMutation()
39
+ const { mutateAsync: publishTemplate } = usePublishTemplateMutation()
40
+ const { mutateAsync: unpublishTemplate } = useUnpublishTemplateMutation()
41
+ const promoteTarget = ref<TemplateWithMessageSummary | null>(null)
42
+ const isPromoteModalOpen = computed({
43
+ get: () => promoteTarget.value !== null,
44
+ set: (v) => {
45
+ if (!v) promoteTarget.value = null
46
+ },
47
+ })
48
+
49
+ const tabItems = computed(() => {
50
+ const items = []
51
+ if (hasWhatsapp.value) {
52
+ items.push({
53
+ label: t('message.render_type.whatsapp_v1'),
54
+ value: 'whatsapp_v1',
55
+ slot: 'whatsapp_v1',
56
+ })
57
+ }
58
+ if (hasEmail.value) {
59
+ items.push({
60
+ label: t('message.render_type.email_v1'),
61
+ value: 'email_v1',
62
+ slot: 'email_v1',
63
+ })
64
+ }
65
+ return items
66
+ })
67
+
68
+ const scopeItems = [
69
+ { label: 'All', value: 'all' as const },
70
+ { label: 'My templates', value: 'mine' as const },
71
+ { label: 'System', value: 'system' as const },
72
+ ]
73
+
74
+ function getLocalizedPreview(tmpl: TemplateWithMessageSummary) {
75
+ const msg =
76
+ tmpl.messages.find((m) => m.language === locale.value) ||
77
+ tmpl.messages.find((m) => m.language === 'en') ||
78
+ tmpl.messages[0]
79
+ return msg?.preview || ''
80
+ }
81
+
82
+ function channelFromRenderType(rt: RenderType): 'email' | 'whatsapp' {
83
+ return rt === 'email_v1' ? 'email' : 'whatsapp'
84
+ }
85
+
86
+ function editTemplate(tmpl: TemplateWithMessageSummary) {
87
+ const channel = channelFromRenderType(tmpl.messages[0]?.render_type || renderType.value)
88
+ navigateTo({ name: `${channel}-template-edit`, params: { id: tmpl.id } })
89
+ }
90
+
91
+ async function handleDelete(tmpl: TemplateWithMessageSummary) {
92
+ await deleteTemplate(tmpl.id)
93
+ toast.add({ title: t('templates.dialogs.delete.success', { name: tmpl.name }), color: 'success' })
94
+ }
95
+
96
+ async function handlePublish(tmpl: TemplateWithMessageSummary) {
97
+ await publishTemplate(tmpl.id)
98
+ toast.add({
99
+ title: t('templates.dialogs.publish.success', { name: tmpl.name }),
100
+ color: 'success',
101
+ })
102
+ }
103
+
104
+ async function handleUnpublish(tmpl: TemplateWithMessageSummary) {
105
+ await unpublishTemplate(tmpl.id)
106
+ toast.add({
107
+ title: t('templates.dialogs.unpublish.success', { name: tmpl.name }),
108
+ color: 'success',
109
+ })
110
+ }
111
+
112
+ function handleUse(tmpl: TemplateWithMessageSummary) {
113
+ const channel = channelFromRenderType(tmpl.messages[0]?.render_type || renderType.value)
114
+ navigateTo({ name: `${channel}-message-create`, query: { from_template: tmpl.id } })
115
+ }
116
+
117
+ function rowActions(tmpl: TemplateWithMessageSummary) {
118
+ const actions = [
119
+ {
120
+ label: t('common.actions.edit'),
121
+ icon: 'ph:pencil-simple-line',
122
+ onSelect: () => editTemplate(tmpl),
123
+ },
124
+ ]
125
+
126
+ if (appContext.isAdmin) {
127
+ if (tmpl.user_id !== SYSTEM_USER_ID) {
128
+ actions.push({
129
+ label: t('templates.dialogs.promote.title'),
130
+ icon: 'ph:arrow-fat-up',
131
+ onSelect: () => {
132
+ promoteTarget.value = tmpl
133
+ },
134
+ })
135
+ }
136
+
137
+ if (tmpl.visibility === 'internal') {
138
+ actions.push({
139
+ label: t('common.actions.publish'),
140
+ icon: 'ph:globe',
141
+ onSelect: () => handlePublish(tmpl),
142
+ })
143
+ } else {
144
+ actions.push({
145
+ label: t('common.actions.unpublish'),
146
+ icon: 'ph:globe-x',
147
+ onSelect: () => handleUnpublish(tmpl),
148
+ })
149
+ }
150
+ }
151
+
152
+ actions.push(
153
+ { label: t('templates.use_template'), icon: 'ph:arrow-right', onSelect: () => handleUse(tmpl) },
154
+ { label: t('common.actions.delete'), icon: 'ph:trash', onSelect: () => handleDelete(tmpl) },
155
+ )
156
+
157
+ return actions
158
+ }
159
+
160
+ function createTemplate() {
161
+ const channel = channelFromRenderType(renderType.value)
162
+ navigateTo({ name: `${channel}-template-create` })
163
+ }
164
+ </script>
165
+
166
+ <template>
167
+ <LayoutBasePage
168
+ :title="$t('templates.title')"
169
+ :tabs="tabItems.length > 1 ? tabItems : undefined"
170
+ :active-tab="renderType"
171
+ @tab-change="renderType = $event as RenderType"
172
+ >
173
+ <template #actions>
174
+ <UButton :label="$t('templates.add_template')" color="primary" @click="createTemplate" />
175
+ </template>
176
+
177
+ <div class="flex-1 space-y-2.5 min-h-0 flex flex-col">
178
+ <div class="flex gap-2">
179
+ <USelect v-model="scope" :items="scopeItems" class="max-w-48" />
180
+ </div>
181
+
182
+ <UCard
183
+ :ui="{
184
+ root: 'flex-1 min-h-0 flex flex-col',
185
+ body: 'overflow-auto relative flex-1 p-0 sm:p-0',
186
+ }"
187
+ >
188
+ <UTable
189
+ :loading="isPending"
190
+ class="flex-1 bg-white"
191
+ :ui="{ tr: 'hover:bg-muted', root: 'overflow-visible static' }"
192
+ :data="templates || []"
193
+ :columns="[
194
+ { id: 'name', accessorKey: 'name', header: $t('pages.message_library.headers.name') },
195
+ {
196
+ id: 'category',
197
+ accessorKey: 'category',
198
+ header: $t('pages.message_library.headers.category'),
199
+ },
200
+ {
201
+ id: 'preview',
202
+ accessorFn: (row: TemplateWithMessageSummary) => getLocalizedPreview(row),
203
+ header: $t('pages.message_library.headers.preview'),
204
+ },
205
+ {
206
+ id: 'languages',
207
+ accessorFn: (row: TemplateWithMessageSummary) => `${row.messages.length} lang`,
208
+ header: 'Languages',
209
+ },
210
+ {
211
+ id: 'updated_at',
212
+ accessorFn: (row: TemplateWithMessageSummary) => formatDate(row.updated_at),
213
+ header: $t('pages.message_library.headers.updated_at'),
214
+ },
215
+ {
216
+ id: 'actions',
217
+ header: '',
218
+ enableSorting: false,
219
+ meta: { class: { th: 'w-[66px]', td: 'w-[66px]' } },
220
+ },
221
+ ]"
222
+ sticky
223
+ >
224
+ <template #name-cell="{ row }">
225
+ <div class="flex items-center gap-2">
226
+ <span>{{ row.original.name }}</span>
227
+ <UBadge
228
+ v-if="row.original.user_id === SYSTEM_USER_ID"
229
+ label="System"
230
+ size="sm"
231
+ color="info"
232
+ variant="soft"
233
+ />
234
+ <UBadge
235
+ v-if="row.original.visibility === 'internal'"
236
+ :label="$t('common.internal')"
237
+ size="sm"
238
+ color="warning"
239
+ variant="soft"
240
+ />
241
+ </div>
242
+ </template>
243
+
244
+ <template #category-cell="{ row }">
245
+ <UBadge :label="row.original.category" size="md" color="neutral" variant="soft" />
246
+ </template>
247
+
248
+ <template #preview-cell="{ row }">
249
+ <div class="line-clamp-2 max-w-[200px] truncate">
250
+ {{ getLocalizedPreview(row.original) }}
251
+ </div>
252
+ </template>
253
+
254
+ <template #actions-cell="{ row }">
255
+ <SMoreActions :actions="rowActions(row.original)" />
256
+ </template>
257
+ </UTable>
258
+ </UCard>
259
+ </div>
260
+
261
+ <TemplateListPromoteModal v-model:open="isPromoteModalOpen" :template="promoteTarget" />
262
+ </LayoutBasePage>
263
+ </template>
@@ -0,0 +1,82 @@
1
+ <script setup lang="ts">
2
+ import type { WhatsappMessage, PostEntity } from '@dev.smartpricing/message-composer-utils/types'
3
+
4
+ definePageMeta({
5
+ name: 'whatsapp-template-edit',
6
+ middleware: ['channel-guard'],
7
+ channel: 'whatsapp',
8
+ })
9
+
10
+ const route = useRoute()
11
+ const composerRef = useTemplateRef('composerRef')
12
+ const store = useWhatsappComposerStore()
13
+ const toast = useToast()
14
+ const { t } = useI18n()
15
+ const { goBackOrFallback } = useGoBack()
16
+
17
+ const templateId = computed(() => route.params.id as string)
18
+
19
+ const { mutateAsync: updateTemplate } = useUpdateTemplateMutation()
20
+ const saving = ref(false)
21
+
22
+ async function handleSave() {
23
+ saving.value = true
24
+ try {
25
+ const messages = Object.values(store.messages).map((msg) => {
26
+ const m = msg as PostEntity<WhatsappMessage>
27
+ return {
28
+ language_id: m.language_id,
29
+ render_type: m.render_type,
30
+ body: m.body,
31
+ metadata: m.metadata || {},
32
+ }
33
+ })
34
+
35
+ await updateTemplate({
36
+ id: templateId.value,
37
+ data: {
38
+ name: store.name,
39
+ category: store.category,
40
+ messages,
41
+ },
42
+ })
43
+
44
+ toast.add({ title: t('templates.dialogs.save_as_template.success'), color: 'success' })
45
+ goBackOrFallback()
46
+ } finally {
47
+ saving.value = false
48
+ }
49
+ }
50
+ </script>
51
+
52
+ <template>
53
+ <LayoutBasePage
54
+ :title="$t('templates.edit_template')"
55
+ :tabs="[
56
+ { label: $t('pages.whatsapp.tabs.content'), value: 'content' },
57
+ { label: $t('pages.whatsapp.tabs.translations'), value: 'translations' },
58
+ ]"
59
+ :active-tab="store.activeTab"
60
+ @tab-change="store.activeTab = $event as 'content' | 'translations'"
61
+ >
62
+ <template #actions>
63
+ <UButton
64
+ @click="composerRef?.handleCancel()"
65
+ :label="$t('common.actions.cancel')"
66
+ color="primary"
67
+ variant="ghost"
68
+ :loading="saving || composerRef?.isTranslating"
69
+ />
70
+ <UButton
71
+ @click="handleSave()"
72
+ :label="$t('common.actions.save')"
73
+ color="primary"
74
+ :loading="saving || composerRef?.isTranslating"
75
+ />
76
+ </template>
77
+
78
+ <ComposerWhatsappComposer ref="composerRef" mode="create" :from-template-id="templateId">
79
+ <template #actions></template>
80
+ </ComposerWhatsappComposer>
81
+ </LayoutBasePage>
82
+ </template>
@@ -0,0 +1,74 @@
1
+ <script setup lang="ts">
2
+ import type { WhatsappMessage, PostEntity } from '@dev.smartpricing/message-composer-utils/types'
3
+
4
+ definePageMeta({
5
+ name: 'whatsapp-template-create',
6
+ middleware: ['channel-guard'],
7
+ channel: 'whatsapp',
8
+ })
9
+
10
+ const composerRef = useTemplateRef('composerRef')
11
+ const store = useWhatsappComposerStore()
12
+ const toast = useToast()
13
+ const { t } = useI18n()
14
+ const { goBackOrFallback } = useGoBack()
15
+
16
+ const { mutateAsync: createTemplate } = useCreateTemplateMutation()
17
+ const saving = ref(false)
18
+
19
+ async function handleSave() {
20
+ saving.value = true
21
+ try {
22
+ const messages = Object.values(store.messages).map((msg) => {
23
+ const m = msg as PostEntity<WhatsappMessage>
24
+ return {
25
+ language_id: m.language_id,
26
+ render_type: m.render_type,
27
+ body: m.body,
28
+ metadata: m.metadata || {},
29
+ }
30
+ })
31
+
32
+ await createTemplate({
33
+ name: store.name,
34
+ category: store.category,
35
+ messages,
36
+ })
37
+
38
+ toast.add({ title: t('templates.dialogs.save_as_template.success'), color: 'success' })
39
+ goBackOrFallback()
40
+ } finally {
41
+ saving.value = false
42
+ }
43
+ }
44
+ </script>
45
+
46
+ <template>
47
+ <LayoutBasePage
48
+ :title="$t('templates.create_template')"
49
+ :tabs="[
50
+ { label: $t('pages.whatsapp.tabs.content'), value: 'content' },
51
+ { label: $t('pages.whatsapp.tabs.translations'), value: 'translations' },
52
+ ]"
53
+ :active-tab="store.activeTab"
54
+ @tab-change="store.activeTab = $event as 'content' | 'translations'"
55
+ >
56
+ <template #actions>
57
+ <UButton
58
+ @click="composerRef?.handleCancel()"
59
+ :label="$t('common.actions.cancel')"
60
+ color="primary"
61
+ variant="ghost"
62
+ :loading="saving || composerRef?.isTranslating"
63
+ />
64
+ <UButton
65
+ @click="handleSave()"
66
+ :label="$t('common.actions.save')"
67
+ color="primary"
68
+ :loading="saving || composerRef?.isTranslating"
69
+ />
70
+ </template>
71
+
72
+ <ComposerWhatsappComposer ref="composerRef" mode="create" />
73
+ </LayoutBasePage>
74
+ </template>
@@ -7,3 +7,4 @@ export * from './meta'
7
7
  export * from './compilation'
8
8
  export * from './dynamicValues'
9
9
  export * from './brands'
10
+ export * from './templates'