@dev.smartpricing/message-composer-layer 4.0.1 → 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.
- package/app/api/index.ts +1 -0
- package/app/api/templates.ts +55 -0
- package/app/components/Common/NewMessageModal.vue +274 -0
- package/app/components/Composer/EmailComposer.vue +31 -1
- package/app/components/Composer/MessageLibrary.vue +71 -2
- package/app/components/Composer/WhatsappComposer.vue +24 -0
- package/app/components/CreateTemplateButton.vue +8 -45
- package/app/components/Email/InlinePreview.vue +8 -1
- package/app/components/TemplateList/BaseTable.vue +7 -0
- package/app/pages/{template → compose}/email/create.vue +10 -1
- package/app/pages/{template → compose}/whatsapp/create.vue +4 -1
- package/app/pages/templates/email/[id].vue +82 -0
- package/app/pages/templates/email/create.vue +86 -0
- package/app/pages/templates/index.vue +244 -0
- package/app/pages/templates/whatsapp/[id].vue +82 -0
- package/app/pages/templates/whatsapp/create.vue +86 -0
- package/app/queries/index.ts +1 -0
- package/app/queries/templates.ts +119 -0
- package/app/utils/testIds.const.ts +1 -0
- package/i18n/locales/de.ts +42 -0
- package/i18n/locales/en.ts +41 -0
- package/i18n/locales/es.ts +41 -0
- package/i18n/locales/fr.ts +41 -0
- package/i18n/locales/it.ts +41 -0
- package/package.json +2 -2
- /package/app/pages/{template → compose}/email/[id].vue +0 -0
- /package/app/pages/{template → compose}/whatsapp/[id].vue +0 -0
package/app/api/index.ts
CHANGED
|
@@ -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
|
}
|
|
@@ -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
|
-
|
|
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 ────────────────────────────────
|
|
@@ -1,59 +1,22 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import
|
|
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
|
|
11
|
-
trackEvent('message_create', { message_kind:
|
|
12
|
-
|
|
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="
|
|
18
|
+
@click="openModal"
|
|
46
19
|
/>
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
:
|
|
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(
|
|
@@ -18,6 +18,7 @@ const emit = defineEmits<{
|
|
|
18
18
|
duplicate: [MessageGroupWithMessageSummary]
|
|
19
19
|
publish: [MessageGroupWithMessageSummary]
|
|
20
20
|
unpublish: [MessageGroupWithMessageSummary]
|
|
21
|
+
saveAsTemplate: [MessageGroupWithMessageSummary]
|
|
21
22
|
}>()
|
|
22
23
|
|
|
23
24
|
const appContext = useAppContextStore()
|
|
@@ -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',
|
|
@@ -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
|
|
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>
|