@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.
@@ -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>
@@ -0,0 +1,86 @@
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 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<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 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.whatsapp.tabs.content'), value: 'content' },
55
+ { label: $t('pages.whatsapp.tabs.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
+ <ComposerWhatsappComposer 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
+ </ComposerWhatsappComposer>
85
+ </LayoutBasePage>
86
+ </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'