@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
package/app/api/index.ts CHANGED
@@ -9,3 +9,4 @@ export * from './compilation'
9
9
  export * from './tracking'
10
10
  export * from './dynamicValues'
11
11
  export * from './brands'
12
+ export * from './templates'
@@ -0,0 +1,55 @@
1
+ import type { TemplateFromDb, RenderType } from '@dev.smartpricing/message-composer-utils/types'
2
+ import type {
3
+ TemplateWithMessages,
4
+ TemplateWithMessageSummary,
5
+ CreateTemplatePayload,
6
+ UpdateTemplatePayload,
7
+ } from '@dev.smartpricing/message-composer-utils/types'
8
+ import { apiClient } from './client'
9
+
10
+ export const templatesApi = {
11
+ getAll: (options?: {
12
+ renderType?: RenderType
13
+ scope?: 'system' | 'mine' | 'all'
14
+ tags?: string[]
15
+ tagsMode?: 'and' | 'or'
16
+ }) => {
17
+ const params = new URLSearchParams()
18
+ if (options?.renderType) params.set('render_type', options.renderType)
19
+ if (options?.scope) params.set('scope', options.scope)
20
+ if (options?.tags && options.tags.length > 0) {
21
+ options.tags.forEach((tag) => params.append('tags', tag))
22
+ }
23
+ if (options?.tagsMode) params.set('tags_mode', options.tagsMode)
24
+
25
+ const query = params.size ? `?${params.toString()}` : ''
26
+ return apiClient<TemplateWithMessageSummary[]>(`/templates${query}`)
27
+ },
28
+
29
+ getById: (id: string) => apiClient<TemplateWithMessages>(`/templates/${id}`),
30
+
31
+ create: (data: CreateTemplatePayload) =>
32
+ apiClient<TemplateFromDb & { tags: string[] }>('/templates', {
33
+ method: 'POST',
34
+ body: data,
35
+ }),
36
+
37
+ update: (id: string, data: UpdateTemplatePayload) =>
38
+ apiClient<TemplateFromDb>(`/templates/${id}`, {
39
+ method: 'PUT',
40
+ body: data,
41
+ }),
42
+
43
+ delete: (id: string) => apiClient(`/templates/${id}`, { method: 'DELETE' }),
44
+
45
+ publish: (id: string) => apiClient<TemplateFromDb>(`/templates/${id}/publish`, { method: 'PUT' }),
46
+
47
+ unpublish: (id: string) =>
48
+ apiClient<TemplateFromDb>(`/templates/${id}/unpublish`, { method: 'PUT' }),
49
+
50
+ saveFromMessageGroup: (messageGroupId: string, name?: string) =>
51
+ apiClient<TemplateFromDb>(`/message-groups/${messageGroupId}/save-as-template`, {
52
+ method: 'POST',
53
+ body: { name },
54
+ }),
55
+ }
@@ -0,0 +1,274 @@
1
+ <script setup lang="ts">
2
+ import { SYSTEM_USER_ID } from '@dev.smartpricing/message-composer-utils/types'
3
+ import type {
4
+ RenderType,
5
+ TemplateWithMessageSummary,
6
+ PostEntity,
7
+ WhatsappMessage,
8
+ EmailMessage,
9
+ BrandFromDb,
10
+ } from '@dev.smartpricing/message-composer-utils/types'
11
+ import type { TemplateWithMessages } from '@dev.smartpricing/message-composer-utils/types'
12
+ import { getEmailEmpty, getWhatsappEmpty } from '@dev.smartpricing/message-composer-utils/utils'
13
+ import { templatesApi } from '~/api/templates'
14
+
15
+ const isOpen = defineModel<boolean>('open', { required: true })
16
+
17
+ const { t, locale } = useI18n()
18
+ const { mainLanguage } = storeToRefs(useAppContextStore())
19
+ const { allowedRenderTypes, defaultRenderType } = useChannels()
20
+
21
+ const renderType = ref<RenderType>(defaultRenderType.value)
22
+ const selectedId = ref<string>() // undefined = "start from scratch"
23
+ const selectedTemplateData = ref<TemplateWithMessages>()
24
+ const loadingPreview = ref(false)
25
+ const brand = ref<BrandFromDb>()
26
+
27
+ const isFromScratch = computed(() => selectedId.value == null)
28
+ const isEmail = computed(() => renderType.value === 'email_v1')
29
+
30
+ const channelItems = computed(() =>
31
+ allowedRenderTypes.value.map((rt) => ({ label: t(`message.render_type.${rt}`), value: rt })),
32
+ )
33
+
34
+ function renderTypeToChannel(rt: RenderType): 'email' | 'whatsapp' {
35
+ return rt === 'email_v1' ? 'email' : 'whatsapp'
36
+ }
37
+
38
+ // Brands query (for email brand selector)
39
+ const { data: brands } = useBrandsQuery()
40
+
41
+ // Templates query
42
+ const { data: templates, isPending: templatesLoading } = useTemplatesQuery(
43
+ computed(() => ({ renderType: renderType.value })),
44
+ )
45
+
46
+ const systemTemplates = computed(() =>
47
+ (templates.value || []).filter((t) => t.user_id === SYSTEM_USER_ID),
48
+ )
49
+
50
+ const myTemplates = computed(() =>
51
+ (templates.value || []).filter((t) => t.user_id !== SYSTEM_USER_ID),
52
+ )
53
+
54
+ // Default empty message for "Start from scratch" preview
55
+ const scratchMessage = computed(() => {
56
+ if (isEmail.value) {
57
+ return getEmailEmpty(mainLanguage.value)
58
+ }
59
+ return getWhatsappEmpty(mainLanguage.value)
60
+ })
61
+
62
+ // Preview message: scratch or selected template
63
+ const previewMessage = computed<PostEntity<EmailMessage> | PostEntity<WhatsappMessage> | undefined>(
64
+ () => {
65
+ if (isFromScratch.value) {
66
+ return scratchMessage.value
67
+ }
68
+
69
+ if (!selectedTemplateData.value?.messages?.length) return undefined
70
+ const msgs = selectedTemplateData.value.messages
71
+ const msg =
72
+ msgs.find((m) => m.language_id === locale.value) ||
73
+ msgs.find((m) => m.language_id === 'en') ||
74
+ msgs[0]
75
+ if (!msg) return undefined
76
+ return {
77
+ render_type: msg.render_type,
78
+ body: msg.body,
79
+ metadata: msg.metadata || {},
80
+ language_id: msg.language_id,
81
+ message_group_id: '',
82
+ status: 'draft' as const,
83
+ owner: '',
84
+ user_id: '',
85
+ } as PostEntity<EmailMessage> | PostEntity<WhatsappMessage>
86
+ },
87
+ )
88
+
89
+ // Footer button label
90
+ const actionLabel = computed(() =>
91
+ isFromScratch.value ? t('templates.start_from_scratch') : t('templates.use_template'),
92
+ )
93
+
94
+ // Reset on open — pre-select "Start from scratch"
95
+ watch(isOpen, (open) => {
96
+ if (open) {
97
+ selectedId.value = undefined
98
+ selectedTemplateData.value = undefined
99
+ brand.value = undefined
100
+ renderType.value = defaultRenderType.value
101
+ }
102
+ })
103
+
104
+ // Reset selection when render type changes
105
+ watch(renderType, () => {
106
+ selectedId.value = undefined
107
+ selectedTemplateData.value = undefined
108
+ brand.value = undefined
109
+ })
110
+
111
+ // Fetch full template when selected
112
+ watch(selectedId, async (id) => {
113
+ if (!id) {
114
+ selectedTemplateData.value = undefined
115
+ return
116
+ }
117
+ loadingPreview.value = true
118
+ try {
119
+ selectedTemplateData.value = await templatesApi.getById(id)
120
+ } finally {
121
+ loadingPreview.value = false
122
+ }
123
+ })
124
+
125
+ function selectScratch() {
126
+ selectedId.value = undefined
127
+ selectedTemplateData.value = undefined
128
+ }
129
+
130
+ function selectTemplate(tmpl: TemplateWithMessageSummary) {
131
+ selectedId.value = selectedId.value === tmpl.id ? undefined : tmpl.id
132
+ }
133
+
134
+ function handleCreate() {
135
+ isOpen.value = false
136
+ const channel = renderTypeToChannel(renderType.value)
137
+ const query: Record<string, string> = {}
138
+ if (selectedId.value) query.from_template = selectedId.value
139
+ if (brand.value) query.brand_id = brand.value.id
140
+ navigateTo({ name: `${channel}-message-create`, query })
141
+ }
142
+ </script>
143
+
144
+ <template>
145
+ <USlideover
146
+ v-model:open="isOpen"
147
+ :title="$t('pages.message_library.add_message_group')"
148
+ side="right"
149
+ :ui="{ content: 'max-w-5xl', body: 'sm:p-0 p-0', footer: 'justify-end' }"
150
+ >
151
+ <template #body>
152
+ <div class="grid grid-cols-[1fr_1fr] divide-x divide-muted min-h-full">
153
+ <!-- Left: Controls + Template list -->
154
+ <div class="overflow-y-auto p-6 space-y-4">
155
+ <!-- Channel selector -->
156
+ <USelect v-if="channelItems.length > 1" v-model="renderType" :items="channelItems" />
157
+
158
+ <!-- Brand selector (email only) -->
159
+ <UFormField
160
+ v-if="isEmail && brands?.length"
161
+ :label="$t('editor.email.generalStyle.brandSection')"
162
+ >
163
+ <USelectMenu v-model="brand" labelKey="name" :items="brands" />
164
+ </UFormField>
165
+
166
+ <!-- Start from scratch -->
167
+ <button
168
+ class="flex w-full items-center gap-3 rounded-lg border-2 border-dashed p-3 text-left text-sm transition"
169
+ :class="
170
+ isFromScratch
171
+ ? 'border-(--ui-primary) bg-(--ui-primary)/5'
172
+ : 'border-(--ui-border-muted) hover:border-(--ui-border) hover:bg-(--ui-bg-elevated)'
173
+ "
174
+ @click="selectScratch"
175
+ >
176
+ <UIcon name="ph:plus-circle" class="text-xl text-(--ui-text-muted)" />
177
+ <span class="font-medium">{{ $t('templates.start_from_scratch') }}</span>
178
+ </button>
179
+
180
+ <!-- Loading -->
181
+ <div v-if="templatesLoading" class="flex justify-center py-4">
182
+ <UIcon name="ph:spinner" class="animate-spin text-xl text-(--ui-text-muted)" />
183
+ </div>
184
+
185
+ <template v-else>
186
+ <!-- My Templates -->
187
+ <div v-if="myTemplates.length > 0">
188
+ <h4 class="mb-2 text-xs font-semibold text-(--ui-text-muted) uppercase">
189
+ {{ $t('templates.my_templates') }}
190
+ </h4>
191
+ <div class="space-y-1">
192
+ <button
193
+ v-for="tmpl in myTemplates"
194
+ :key="tmpl.id"
195
+ class="flex w-full items-center justify-between rounded-md p-2.5 text-left text-sm transition"
196
+ :class="
197
+ selectedId === tmpl.id
198
+ ? 'bg-(--ui-primary)/10 ring-1 ring-(--ui-primary)'
199
+ : 'hover:bg-(--ui-bg-elevated)'
200
+ "
201
+ @click="selectTemplate(tmpl)"
202
+ >
203
+ <div class="flex items-center gap-2">
204
+ <span>{{ tmpl.name }}</span>
205
+ <UBadge :label="tmpl.category" size="sm" color="neutral" variant="soft" />
206
+ </div>
207
+ <UBadge
208
+ :label="`${tmpl.messages.length} lang`"
209
+ size="sm"
210
+ color="info"
211
+ variant="soft"
212
+ />
213
+ </button>
214
+ </div>
215
+ </div>
216
+
217
+ <!-- System Templates -->
218
+ <div v-if="systemTemplates.length > 0">
219
+ <h4 class="mb-2 text-xs font-semibold text-(--ui-text-muted) uppercase">
220
+ {{ $t('templates.system_templates') }}
221
+ </h4>
222
+ <div class="space-y-1">
223
+ <button
224
+ v-for="tmpl in systemTemplates"
225
+ :key="tmpl.id"
226
+ class="flex w-full items-center justify-between rounded-md p-2.5 text-left text-sm transition"
227
+ :class="
228
+ selectedId === tmpl.id
229
+ ? 'bg-(--ui-primary)/10 ring-1 ring-(--ui-primary)'
230
+ : 'hover:bg-(--ui-bg-elevated)'
231
+ "
232
+ @click="selectTemplate(tmpl)"
233
+ >
234
+ <div class="flex items-center gap-2">
235
+ <span>{{ tmpl.name }}</span>
236
+ <UBadge :label="tmpl.category" size="sm" color="neutral" variant="soft" />
237
+ </div>
238
+ <UBadge
239
+ :label="`${tmpl.messages.length} lang`"
240
+ size="sm"
241
+ color="info"
242
+ variant="soft"
243
+ />
244
+ </button>
245
+ </div>
246
+ </div>
247
+ </template>
248
+ </div>
249
+
250
+ <!-- Right: Preview -->
251
+ <div class="bg-neutral-50 overflow-y-auto">
252
+ <div v-if="loadingPreview" class="flex items-center justify-center h-full">
253
+ <UIcon name="ph:spinner" class="animate-spin text-xl text-(--ui-text-muted)" />
254
+ </div>
255
+
256
+ <div v-else-if="previewMessage && !isEmail" class="p-6">
257
+ <WhatsappPreview :message="previewMessage as PostEntity<WhatsappMessage>" />
258
+ </div>
259
+
260
+ <div v-else-if="previewMessage && isEmail" class="p-6">
261
+ <EmailInlinePreview
262
+ :message="previewMessage as PostEntity<EmailMessage>"
263
+ :brand="brand"
264
+ />
265
+ </div>
266
+ </div>
267
+ </div>
268
+ </template>
269
+
270
+ <template #footer>
271
+ <UButton :label="actionLabel" color="primary" @click="handleCreate" />
272
+ </template>
273
+ </USlideover>
274
+ </template>
@@ -8,10 +8,13 @@ import {
8
8
  } from '@dev.smartpricing/message-composer-utils/utils'
9
9
  import { useAppContextStore } from '@/stores/appContext'
10
10
  import { useMessageGroupQuery, useLanguagesQuery } from '~/queries'
11
+ import { templatesApi } from '~/api/templates'
11
12
 
12
13
  const props = defineProps<{
13
14
  mode: 'create' | 'edit'
14
15
  messageGroupId?: string
16
+ fromTemplateId?: string
17
+ brandId?: string
15
18
  }>()
16
19
 
17
20
  const emit = defineEmits<{
@@ -28,7 +31,34 @@ const { goBackOrFallback } = useGoBack()
28
31
  // ── Init ──────────────────────────────────────────────────────────
29
32
  if (props.mode === 'create') {
30
33
  store.initCreate(mainLanguage.value)
31
- emailEditorStore.initBrand()
34
+ emailEditorStore.initBrand(props.brandId)
35
+
36
+ // Pre-fill from template if provided
37
+ if (props.fromTemplateId) {
38
+ templatesApi.getById(props.fromTemplateId).then((template) => {
39
+ if (!template?.messages?.length) return
40
+ store.name = template.name
41
+ store.category = template.category
42
+ for (const msg of template.messages) {
43
+ if (msg.render_type !== 'email_v1') continue
44
+ store.messages[msg.language_id] = {
45
+ render_type: msg.render_type,
46
+ body: msg.body,
47
+ metadata: {},
48
+ language_id: msg.language_id,
49
+ message_group_id: '',
50
+ status: 'draft',
51
+ owner: '',
52
+ user_id: '',
53
+ } as PostEntity<EmailMessage>
54
+ }
55
+ // Reload editor with first message
56
+ const currentMessage = store.currentMessage
57
+ if (currentMessage) {
58
+ emailEditorStore.loadFromData(currentMessage as PostEntity<EmailMessage>)
59
+ }
60
+ })
61
+ }
32
62
  } else {
33
63
  // Edit mode: data loaded via watcher below
34
64
  }
@@ -17,7 +17,7 @@ const emit = defineEmits<{
17
17
 
18
18
  const store = useEmailComposerStore()
19
19
  const emailEditorStore = useEmailEditorStore()
20
- const adminStore = useAdminStore()
20
+ const appContext = useAppContextStore()
21
21
 
22
22
  const testIds = computed(() =>
23
23
  props.mode === 'create' ? templateEmailCreateTestIds : templateEmailEditTestIds,
@@ -67,7 +67,7 @@ const testIds = computed(() =>
67
67
  </UFieldGroup>
68
68
 
69
69
  <USwitch
70
- v-if="mode === 'create' && adminStore.isAdminModeActive"
70
+ v-if="mode === 'create' && appContext.isAdmin"
71
71
  :label="$t('common.internal')"
72
72
  v-model="store.isInternal"
73
73
  color="info"
@@ -4,13 +4,14 @@ import {
4
4
  useDeleteMessageGroupMutation,
5
5
  usePublishMessageGroupMutation,
6
6
  useUnpublishMessageGroupMutation,
7
+ useSaveAsTemplateMutation,
7
8
  } from '~/queries'
8
9
  import type {
9
10
  MessageGroupWithMessageSummary,
10
11
  RenderType,
11
12
  } from '@dev.smartpricing/message-composer-utils/types'
12
13
  import type { MessageActionProperties, MessagesKind } from '@/types/tracking'
13
- import { computed } from 'vue'
14
+ import { computed, ref } from 'vue'
14
15
  import BaseTable from '@/components/TemplateList/BaseTable.vue'
15
16
 
16
17
  const renderType = defineModel<RenderType>('renderType', { required: true })
@@ -42,6 +43,34 @@ const { confirm } = useConfirm()
42
43
  const { mutateAsync: deleteGroup } = useDeleteMessageGroupMutation()
43
44
  const { mutateAsync: publishGroup } = usePublishMessageGroupMutation()
44
45
  const { mutateAsync: unpublishGroup } = useUnpublishMessageGroupMutation()
46
+ const { mutateAsync: saveAsTemplate, isPending: isSavingAsTemplate } = useSaveAsTemplateMutation()
47
+
48
+ // Save as template modal state
49
+ const saveAsTemplateModalOpen = ref(false)
50
+ const saveAsTemplateName = ref('')
51
+ const saveAsTemplateTarget = ref<MessageGroupWithMessageSummary | null>(null)
52
+
53
+ function openSaveAsTemplateModal(row: MessageGroupWithMessageSummary) {
54
+ saveAsTemplateTarget.value = row
55
+ saveAsTemplateName.value = row.name
56
+ saveAsTemplateModalOpen.value = true
57
+ }
58
+
59
+ async function confirmSaveAsTemplate() {
60
+ if (!saveAsTemplateTarget.value) return
61
+
62
+ const row = saveAsTemplateTarget.value
63
+ const name = saveAsTemplateName.value.trim() || row.name
64
+
65
+ await saveAsTemplate({ messageGroupId: row.id, name })
66
+
67
+ useToast().add({
68
+ title: t('templates.dialogs.save_as_template.success'),
69
+ color: 'success',
70
+ })
71
+
72
+ saveAsTemplateModalOpen.value = false
73
+ }
45
74
 
46
75
  function getMessageTrackingProps(row: MessageGroupWithMessageSummary): MessageActionProperties {
47
76
  return {
@@ -135,7 +164,7 @@ function navigate(row: MessageGroupWithMessageSummary) {
135
164
  trackEvent('message_edit', getMessageTrackingProps(row))
136
165
  emit('edit', row)
137
166
  const type = row.messages[0]?.render_type === 'email_v1' ? 'email' : 'whatsapp'
138
- router.push({ name: `${type}-message-edit`, params: { id: row.id } })
167
+ navigateTo({ name: `${type}-message-edit`, params: { id: row.id } })
139
168
  }
140
169
 
141
170
  defineExpose({ execute })
@@ -151,5 +180,45 @@ defineExpose({ execute })
151
180
  @duplicate="handleDuplicate"
152
181
  @publish="askPublishGroup"
153
182
  @unpublish="askUnpublishGroup"
183
+ @save-as-template="openSaveAsTemplateModal"
154
184
  />
185
+
186
+ <UModal
187
+ v-model:open="saveAsTemplateModalOpen"
188
+ :title="t('templates.dialogs.save_as_template.title')"
189
+ close-icon="ph:x"
190
+ >
191
+ <template #body>
192
+ <div class="flex flex-col gap-4">
193
+ <p class="text-sm text-muted">
194
+ {{ t('templates.dialogs.save_as_template.message') }}
195
+ </p>
196
+ <UFormField :label="t('templates.template_name')">
197
+ <UInput
198
+ v-model="saveAsTemplateName"
199
+ :placeholder="t('templates.template_name_placeholder')"
200
+ class="w-full"
201
+ autofocus
202
+ @keydown.enter="confirmSaveAsTemplate"
203
+ />
204
+ </UFormField>
205
+ </div>
206
+ </template>
207
+
208
+ <template #footer>
209
+ <div class="flex gap-2 justify-end w-full">
210
+ <UButton variant="outline" color="neutral" @click="saveAsTemplateModalOpen = false">
211
+ {{ t('common.actions.cancel') }}
212
+ </UButton>
213
+ <UButton
214
+ color="primary"
215
+ :loading="isSavingAsTemplate"
216
+ :disabled="!saveAsTemplateName.trim()"
217
+ @click="confirmSaveAsTemplate"
218
+ >
219
+ {{ t('templates.save_as_template') }}
220
+ </UButton>
221
+ </div>
222
+ </template>
223
+ </UModal>
155
224
  </template>
@@ -9,11 +9,13 @@ import type {
9
9
  import { useAppContextStore } from '@/stores/appContext'
10
10
  import { useMetaSubmissionStore } from '@/stores/metaSubmission'
11
11
  import { useMessageGroupQuery, useLanguagesQuery } from '~/queries'
12
+ import { templatesApi } from '~/api/templates'
12
13
  import type { WaHeaderType } from '~/types/tracking'
13
14
 
14
15
  const props = defineProps<{
15
16
  mode: 'create' | 'edit'
16
17
  messageGroupId?: string
18
+ fromTemplateId?: string
17
19
  }>()
18
20
 
19
21
  const emit = defineEmits<{
@@ -30,6 +32,28 @@ const { submitToMeta } = useMetaSubmissionStore()
30
32
  // ── Init ──────────────────────────────────────────────────────────
31
33
  if (props.mode === 'create') {
32
34
  store.initCreate(mainLanguage.value)
35
+
36
+ // Pre-fill from template if provided
37
+ if (props.fromTemplateId) {
38
+ templatesApi.getById(props.fromTemplateId).then((template) => {
39
+ if (!template?.messages?.length) return
40
+ store.name = template.name
41
+ store.category = template.category
42
+ for (const msg of template.messages) {
43
+ if (msg.render_type !== 'whatsapp_v1') continue
44
+ store.messages[msg.language_id] = {
45
+ render_type: msg.render_type,
46
+ body: msg.body,
47
+ metadata: {},
48
+ language_id: msg.language_id,
49
+ message_group_id: '',
50
+ status: 'draft',
51
+ owner: '',
52
+ user_id: '',
53
+ } as PostEntity<WhatsappMessage>
54
+ }
55
+ })
56
+ }
33
57
  }
34
58
 
35
59
  // ── Edit mode: fetch message group ────────────────────────────────
@@ -13,7 +13,7 @@ const emit = defineEmits<{
13
13
  }>()
14
14
 
15
15
  const store = useWhatsappComposerStore()
16
- const adminStore = useAdminStore()
16
+ const appContext = useAppContextStore()
17
17
 
18
18
  const testIds = computed(() =>
19
19
  props.mode === 'create' ? templateWhatsappCreateTestIds : templateWhatsappEditTestIds,
@@ -55,7 +55,7 @@ const testIds = computed(() =>
55
55
  </UFieldGroup>
56
56
 
57
57
  <USwitch
58
- v-if="mode === 'create' && adminStore.isAdminModeActive"
58
+ v-if="mode === 'create' && appContext.isAdmin"
59
59
  v-model="store.isInternal"
60
60
  :label="$t('common.internal')"
61
61
  color="info"
@@ -1,59 +1,22 @@
1
1
  <script setup lang="ts">
2
- import { computed } from 'vue'
3
- import { useRouter } from 'vue-router'
2
+ import NewMessageModal from '@/components/Common/NewMessageModal.vue'
4
3
 
5
- const router = useRouter()
6
- const { hasWhatsapp, hasEmail } = useChannels()
7
- const { t } = useI18n()
8
4
  const { trackEvent } = useTracking()
5
+ const modalOpen = ref(false)
9
6
 
10
- function createTemplate(type: 'whatsapp' | 'email') {
11
- trackEvent('message_create', { message_kind: type })
12
- router.push({ name: `${type}-message-create` })
7
+ function openModal() {
8
+ trackEvent('message_create', { message_kind: 'open_modal' })
9
+ modalOpen.value = true
13
10
  }
14
-
15
- const templateOptions = computed(() => {
16
- const options = []
17
- if (hasWhatsapp.value) {
18
- options.push({
19
- label: t('message.render_type.whatsapp_v1'),
20
- onSelect: () => createTemplate('whatsapp'),
21
- })
22
- }
23
- if (hasEmail.value) {
24
- options.push({
25
- label: t('message.render_type.email_v1'),
26
- onSelect: () => createTemplate('email'),
27
- })
28
- }
29
- return options
30
- })
31
-
32
- const singleChannelType = computed(() => {
33
- if (hasWhatsapp.value && !hasEmail.value) return 'whatsapp'
34
- if (hasEmail.value && !hasWhatsapp.value) return 'email'
35
- return null
36
- })
37
11
  </script>
38
12
 
39
13
  <template>
40
14
  <UButton
41
- v-if="singleChannelType"
42
15
  :label="$t('pages.message_library.add_message_group')"
43
16
  color="primary"
44
17
  :data-testid="homeTestIds.createTemplateButton"
45
- @click="createTemplate(singleChannelType)"
18
+ @click="openModal"
46
19
  />
47
- <UDropdownMenu
48
- v-else
49
- :items="templateOptions"
50
- :popper="{ placement: 'bottom-end' }"
51
- :data-testid="homeTestIds.createTemplateDropdown"
52
- >
53
- <UButton
54
- :label="$t('pages.message_library.add_message_group')"
55
- color="primary"
56
- :data-testid="homeTestIds.createTemplateButton"
57
- />
58
- </UDropdownMenu>
20
+
21
+ <NewMessageModal v-model:open="modalOpen" />
59
22
  </template>
@@ -44,7 +44,7 @@ const previewEmail = useDebounceFn(() => {
44
44
  default_socials: normalizeBrandSocials(props.brand.default_socials),
45
45
  logo_url: props.brand.logo_url,
46
46
  }
47
- : null,
47
+ : undefined,
48
48
  })
49
49
  }, 0)
50
50
 
@@ -65,6 +65,13 @@ watch(
65
65
  { immediate: true },
66
66
  )
67
67
 
68
+ watch(
69
+ () => props.brand,
70
+ () => {
71
+ previewEmail()
72
+ },
73
+ )
74
+
68
75
  const iframeContent = ref<string>('')
69
76
 
70
77
  watch(
@@ -21,7 +21,7 @@ const props = withDefaults(
21
21
  )
22
22
 
23
23
  const iframeWidth = computed(() => {
24
- return props.view === 'mobile' ? '400px' : '600px'
24
+ return props.view === 'mobile' ? '400px' : '800px'
25
25
  })
26
26
 
27
27
  // Extract parameters directly from message
@@ -108,7 +108,6 @@ watch(
108
108
  <HtmlPreviewIframe
109
109
  :html="iframeContent"
110
110
  title="Email preview"
111
- :min-height="600"
112
111
  class="border-0 bg-white w-full"
113
112
  :data-testid="emailPreviewTestIds.iframe"
114
113
  />