@dev.smartpricing/message-composer-layer 1.0.27 → 4.0.2-templates.0

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.
Files changed (35) hide show
  1. package/app/api/index.ts +1 -0
  2. package/app/api/templates.ts +55 -0
  3. package/app/components/Common/NewMessageModal.vue +274 -0
  4. package/app/components/Composer/EmailComposer.vue +31 -1
  5. package/app/components/Composer/EmailContent.vue +2 -2
  6. package/app/components/Composer/MessageLibrary.vue +71 -2
  7. package/app/components/Composer/WhatsappComposer.vue +24 -0
  8. package/app/components/Composer/WhatsappContent.vue +2 -2
  9. package/app/components/CreateTemplateButton.vue +8 -45
  10. package/app/components/Email/InlinePreview.vue +8 -1
  11. package/app/components/Email/Preview.vue +1 -2
  12. package/app/components/TemplateList/BaseTable.vue +11 -4
  13. package/app/composables/composerContext.ts +7 -0
  14. package/app/composables/useTracking.ts +2 -3
  15. package/app/pages/{template → compose}/email/create.vue +10 -1
  16. package/app/pages/{template → compose}/whatsapp/create.vue +4 -1
  17. package/app/pages/templates/email/[id].vue +82 -0
  18. package/app/pages/templates/email/create.vue +86 -0
  19. package/app/pages/templates/index.vue +244 -0
  20. package/app/pages/templates/whatsapp/[id].vue +82 -0
  21. package/app/pages/templates/whatsapp/create.vue +86 -0
  22. package/app/queries/index.ts +1 -0
  23. package/app/queries/templates.ts +119 -0
  24. package/app/stores/appContext.ts +2 -0
  25. package/app/utils/testIds.const.ts +1 -0
  26. package/i18n/locales/de.ts +42 -0
  27. package/i18n/locales/en.ts +41 -0
  28. package/i18n/locales/es.ts +41 -0
  29. package/i18n/locales/fr.ts +41 -0
  30. package/i18n/locales/it.ts +41 -0
  31. package/package.json +2 -3
  32. package/app/plugins/admin-shortcut.client.ts +0 -7
  33. package/app/stores/adminStore.ts +0 -50
  34. /package/app/pages/{template → compose}/email/[id].vue +0 -0
  35. /package/app/pages/{template → compose}/whatsapp/[id].vue +0 -0
@@ -18,9 +18,10 @@ const emit = defineEmits<{
18
18
  duplicate: [MessageGroupWithMessageSummary]
19
19
  publish: [MessageGroupWithMessageSummary]
20
20
  unpublish: [MessageGroupWithMessageSummary]
21
+ saveAsTemplate: [MessageGroupWithMessageSummary]
21
22
  }>()
22
23
 
23
- const adminStore = useAdminStore()
24
+ const appContext = useAppContextStore()
24
25
  const { trackEvent } = useTracking()
25
26
 
26
27
  const { t, locale } = useI18n()
@@ -44,7 +45,7 @@ function getLocalizedMessage(row: MessageGroupWithMessageSummary) {
44
45
 
45
46
  function rowActions(row: MessageGroupWithMessageSummary) {
46
47
  const publishAction =
47
- adminStore.isAdminModeActive && row.visibility === 'internal'
48
+ appContext.isAdmin && row.visibility === 'internal'
48
49
  ? {
49
50
  label: t('common.actions.publish'),
50
51
  icon: 'ph:globe',
@@ -52,7 +53,7 @@ function rowActions(row: MessageGroupWithMessageSummary) {
52
53
  onSelect: () => emit('publish', row),
53
54
  'data-testid': `${templateListTableTestIds.rowPublishAction}-${row.id}`,
54
55
  }
55
- : adminStore.isAdminModeActive && row.visibility === 'public'
56
+ : appContext.isAdmin && row.visibility === 'public'
56
57
  ? {
57
58
  label: t('common.actions.unpublish'),
58
59
  icon: 'ph:globe-x',
@@ -76,6 +77,12 @@ function rowActions(row: MessageGroupWithMessageSummary) {
76
77
  onSelect: () => emit('duplicate', row),
77
78
  'data-testid': `${templateListTableTestIds.rowDuplicateAction}-${row.id}`,
78
79
  },
80
+ {
81
+ label: t('templates.save_as_template'),
82
+ icon: 'ph:bookmark-simple',
83
+ onSelect: () => emit('saveAsTemplate', row),
84
+ 'data-testid': `${templateListTableTestIds.rowSaveAsTemplateAction}-${row.id}`,
85
+ },
79
86
  {
80
87
  label: t('common.actions.delete'),
81
88
  icon: 'ph:trash',
@@ -158,7 +165,7 @@ const props = defineProps<{
158
165
  }>()
159
166
 
160
167
  const filteredMessageGroups = computed(() => {
161
- if (adminStore.isAdminModeActive) return props.messageGroups
168
+ if (appContext.isAdmin) return props.messageGroups
162
169
  return props.messageGroups.filter((mg) => mg.visibility !== 'internal')
163
170
  })
164
171
 
@@ -13,6 +13,7 @@ export interface ComposerContext {
13
13
  default_tags?: Ref<string[]>
14
14
  metaWabaId?: Ref<string>
15
15
  metaAccessToken?: Ref<string>
16
+ isAdmin?: Ref<boolean>
16
17
  }
17
18
 
18
19
  export const COMPOSER_CONTEXT_KEY: InjectionKey<ComposerContext> = Symbol.for('composer-context')
@@ -23,7 +24,13 @@ export function defineComposerContext(context: ComposerContext): void {
23
24
  useNuxtApp().vueApp.provide(COMPOSER_CONTEXT_KEY, context)
24
25
  }
25
26
 
27
+ const SERVER_FALLBACK: ComposerContext = {
28
+ authenticationToken: ref(''),
29
+ }
30
+
26
31
  export function useComposerContext(): ComposerContext {
32
+ if (import.meta.server) return SERVER_FALLBACK
33
+
27
34
  // runWithContext ensures inject() can reach app-level provides
28
35
  // even outside component setup (e.g. Pinia stores, middleware).
29
36
  const context = useNuxtApp().vueApp.runWithContext(() => inject(COMPOSER_CONTEXT_KEY))
@@ -1,15 +1,14 @@
1
1
  import type { TrackingEventMap } from '~/types/tracking'
2
2
 
3
3
  export function useTracking() {
4
- const { product, mainLanguage } = storeToRefs(useAppContextStore())
5
- const { isAdminModeActive } = storeToRefs(useAdminStore())
4
+ const { product, mainLanguage, isAdmin } = storeToRefs(useAppContextStore())
6
5
 
7
6
  function getDefaultProperties(): Record<string, unknown> {
8
7
  return {
9
8
  event_origin: 'message-composer',
10
9
  product: product.value,
11
10
  main_language: mainLanguage.value,
12
- is_admin: isAdminModeActive.value,
11
+ is_admin: isAdmin.value,
13
12
  }
14
13
  }
15
14
 
@@ -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,86 @@
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 appContext = useAppContextStore()
17
+ const isSystem = ref(false)
18
+
19
+ const { mutateAsync: createTemplate } = useCreateTemplateMutation()
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 createTemplate({
36
+ name: store.name,
37
+ category: store.category,
38
+ messages,
39
+ ...(appContext.isAdmin && isSystem.value ? { is_system: true } : {}),
40
+ })
41
+
42
+ toast.add({ title: t('templates.dialogs.save_as_template.success'), color: 'success' })
43
+ goBackOrFallback()
44
+ } finally {
45
+ saving.value = false
46
+ }
47
+ }
48
+ </script>
49
+
50
+ <template>
51
+ <LayoutBasePage
52
+ :title="$t('templates.create_template')"
53
+ :tabs="[
54
+ { label: $t('pages.email.create_message_group.tab_content'), value: 'content' },
55
+ { label: $t('pages.email.create_message_group.tab_translations'), value: 'translations' },
56
+ ]"
57
+ :active-tab="store.activeTab"
58
+ @tab-change="store.activeTab = $event as 'content' | 'translations'"
59
+ >
60
+ <template #actions>
61
+ <UButton
62
+ @click="composerRef?.handleCancel()"
63
+ :label="$t('common.actions.cancel')"
64
+ color="primary"
65
+ variant="ghost"
66
+ :loading="saving || composerRef?.isTranslating"
67
+ />
68
+ <UButton
69
+ @click="handleSave()"
70
+ :label="$t('common.actions.save')"
71
+ color="primary"
72
+ :loading="saving || composerRef?.isTranslating"
73
+ />
74
+ </template>
75
+
76
+ <ComposerEmailComposer ref="composerRef" mode="create">
77
+ <template #actions>
78
+ <UCheckbox
79
+ v-if="appContext.isAdmin"
80
+ v-model="isSystem"
81
+ :label="$t('templates.system_template')"
82
+ />
83
+ </template>
84
+ </ComposerEmailComposer>
85
+ </LayoutBasePage>
86
+ </template>
@@ -0,0 +1,244 @@
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
+
42
+ const tabItems = computed(() => {
43
+ const items = []
44
+ if (hasWhatsapp.value) {
45
+ items.push({
46
+ label: t('message.render_type.whatsapp_v1'),
47
+ value: 'whatsapp_v1',
48
+ slot: 'whatsapp_v1',
49
+ })
50
+ }
51
+ if (hasEmail.value) {
52
+ items.push({
53
+ label: t('message.render_type.email_v1'),
54
+ value: 'email_v1',
55
+ slot: 'email_v1',
56
+ })
57
+ }
58
+ return items
59
+ })
60
+
61
+ const scopeItems = [
62
+ { label: 'All', value: 'all' as const },
63
+ { label: 'My templates', value: 'mine' as const },
64
+ { label: 'System', value: 'system' as const },
65
+ ]
66
+
67
+ function getLocalizedPreview(tmpl: TemplateWithMessageSummary) {
68
+ const msg =
69
+ tmpl.messages.find((m) => m.language === locale.value) ||
70
+ tmpl.messages.find((m) => m.language === 'en') ||
71
+ tmpl.messages[0]
72
+ return msg?.preview || ''
73
+ }
74
+
75
+ function channelFromRenderType(rt: RenderType): 'email' | 'whatsapp' {
76
+ return rt === 'email_v1' ? 'email' : 'whatsapp'
77
+ }
78
+
79
+ function editTemplate(tmpl: TemplateWithMessageSummary) {
80
+ const channel = channelFromRenderType(tmpl.messages[0]?.render_type || renderType.value)
81
+ navigateTo({ name: `${channel}-template-edit`, params: { id: tmpl.id } })
82
+ }
83
+
84
+ async function handleDelete(tmpl: TemplateWithMessageSummary) {
85
+ await deleteTemplate(tmpl.id)
86
+ toast.add({ title: t('templates.dialogs.delete.success', { name: tmpl.name }), color: 'success' })
87
+ }
88
+
89
+ async function handlePublish(tmpl: TemplateWithMessageSummary) {
90
+ await publishTemplate(tmpl.id)
91
+ toast.add({
92
+ title: t('templates.dialogs.publish.success', { name: tmpl.name }),
93
+ color: 'success',
94
+ })
95
+ }
96
+
97
+ async function handleUnpublish(tmpl: TemplateWithMessageSummary) {
98
+ await unpublishTemplate(tmpl.id)
99
+ toast.add({
100
+ title: t('templates.dialogs.unpublish.success', { name: tmpl.name }),
101
+ color: 'success',
102
+ })
103
+ }
104
+
105
+ function handleUse(tmpl: TemplateWithMessageSummary) {
106
+ const channel = channelFromRenderType(tmpl.messages[0]?.render_type || renderType.value)
107
+ navigateTo({ name: `${channel}-message-create`, query: { from_template: tmpl.id } })
108
+ }
109
+
110
+ function rowActions(tmpl: TemplateWithMessageSummary) {
111
+ const actions = [
112
+ {
113
+ label: t('common.actions.edit'),
114
+ icon: 'ph:pencil-simple-line',
115
+ onSelect: () => editTemplate(tmpl),
116
+ },
117
+ ]
118
+
119
+ if (appContext.isAdmin) {
120
+ if (tmpl.visibility === 'internal') {
121
+ actions.push({
122
+ label: t('common.actions.publish'),
123
+ icon: 'ph:globe',
124
+ onSelect: () => handlePublish(tmpl),
125
+ })
126
+ } else {
127
+ actions.push({
128
+ label: t('common.actions.unpublish'),
129
+ icon: 'ph:globe-x',
130
+ onSelect: () => handleUnpublish(tmpl),
131
+ })
132
+ }
133
+ }
134
+
135
+ actions.push(
136
+ { label: t('templates.use_template'), icon: 'ph:arrow-right', onSelect: () => handleUse(tmpl) },
137
+ { label: t('common.actions.delete'), icon: 'ph:trash', onSelect: () => handleDelete(tmpl) },
138
+ )
139
+
140
+ return actions
141
+ }
142
+
143
+ function createTemplate() {
144
+ const channel = channelFromRenderType(renderType.value)
145
+ navigateTo({ name: `${channel}-template-create` })
146
+ }
147
+ </script>
148
+
149
+ <template>
150
+ <LayoutBasePage
151
+ :title="$t('templates.title')"
152
+ :tabs="tabItems.length > 1 ? tabItems : undefined"
153
+ :active-tab="renderType"
154
+ @tab-change="renderType = $event as RenderType"
155
+ >
156
+ <template #actions>
157
+ <UButton :label="$t('templates.add_template')" color="primary" @click="createTemplate" />
158
+ </template>
159
+
160
+ <div class="flex-1 space-y-2.5 min-h-0 flex flex-col">
161
+ <div class="flex gap-2">
162
+ <USelect v-model="scope" :items="scopeItems" class="max-w-48" />
163
+ </div>
164
+
165
+ <UCard
166
+ :ui="{
167
+ root: 'flex-1 min-h-0 flex flex-col',
168
+ body: 'overflow-auto relative flex-1 p-0 sm:p-0',
169
+ }"
170
+ >
171
+ <UTable
172
+ :loading="isPending"
173
+ class="flex-1 bg-white"
174
+ :ui="{ tr: 'hover:bg-muted', root: 'overflow-visible static' }"
175
+ :data="templates || []"
176
+ :columns="[
177
+ { id: 'name', accessorKey: 'name', header: $t('pages.message_library.headers.name') },
178
+ {
179
+ id: 'category',
180
+ accessorKey: 'category',
181
+ header: $t('pages.message_library.headers.category'),
182
+ },
183
+ {
184
+ id: 'preview',
185
+ accessorFn: (row: TemplateWithMessageSummary) => getLocalizedPreview(row),
186
+ header: $t('pages.message_library.headers.preview'),
187
+ },
188
+ {
189
+ id: 'languages',
190
+ accessorFn: (row: TemplateWithMessageSummary) => `${row.messages.length} lang`,
191
+ header: 'Languages',
192
+ },
193
+ {
194
+ id: 'updated_at',
195
+ accessorFn: (row: TemplateWithMessageSummary) => formatDate(row.updated_at),
196
+ header: $t('pages.message_library.headers.updated_at'),
197
+ },
198
+ {
199
+ id: 'actions',
200
+ header: '',
201
+ enableSorting: false,
202
+ meta: { class: { th: 'w-[66px]', td: 'w-[66px]' } },
203
+ },
204
+ ]"
205
+ sticky
206
+ >
207
+ <template #name-cell="{ row }">
208
+ <div class="flex items-center gap-2">
209
+ <span>{{ row.original.name }}</span>
210
+ <UBadge
211
+ v-if="row.original.user_id === SYSTEM_USER_ID"
212
+ label="System"
213
+ size="sm"
214
+ color="info"
215
+ variant="soft"
216
+ />
217
+ <UBadge
218
+ v-if="row.original.visibility === 'internal'"
219
+ :label="$t('common.internal')"
220
+ size="sm"
221
+ color="warning"
222
+ variant="soft"
223
+ />
224
+ </div>
225
+ </template>
226
+
227
+ <template #category-cell="{ row }">
228
+ <UBadge :label="row.original.category" size="md" color="neutral" variant="soft" />
229
+ </template>
230
+
231
+ <template #preview-cell="{ row }">
232
+ <div class="line-clamp-2 max-w-[200px] truncate">
233
+ {{ getLocalizedPreview(row.original) }}
234
+ </div>
235
+ </template>
236
+
237
+ <template #actions-cell="{ row }">
238
+ <SMoreActions :actions="rowActions(row.original)" />
239
+ </template>
240
+ </UTable>
241
+ </UCard>
242
+ </div>
243
+ </LayoutBasePage>
244
+ </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>