@dev.smartpricing/message-composer-layer 1.0.12 → 1.0.14
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/components/Composer/BrandList.vue +190 -0
- package/app/components/Composer/EmailComposer.vue +501 -0
- package/app/components/Composer/EmailContent.vue +95 -0
- package/app/components/Composer/EmailTranslations.vue +70 -0
- package/app/components/Composer/ImageGallery.vue +141 -0
- package/app/components/Composer/MessageLibrary.vue +155 -0
- package/app/components/Composer/WhatsappComposer.vue +424 -0
- package/app/components/Composer/WhatsappContent.vue +88 -0
- package/app/components/Composer/WhatsappTranslations.vue +68 -0
- package/app/components/CreateTemplateButton.vue +1 -1
- package/app/components/Layout/BasePage.vue +34 -34
- package/app/composables/useGoBack.ts +1 -1
- package/app/layouts/default.vue +3 -3
- package/app/middleware/channel-guard.ts +3 -6
- package/app/pages/brands.vue +7 -176
- package/app/pages/images.vue +5 -129
- package/app/pages/index.vue +6 -138
- package/app/pages/preview/[id].vue +1 -0
- package/app/pages/template/email/[id].vue +17 -545
- package/app/pages/template/email/create.vue +15 -514
- package/app/pages/template/whatsapp/[id].vue +22 -509
- package/app/pages/template/whatsapp/create.vue +15 -426
- package/app/stores/emailComposer.ts +188 -0
- package/app/stores/whatsappComposer.ts +173 -0
- package/app/types/page-meta.d.ts +7 -0
- package/package.json +2 -2
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
export default defineNuxtRouteMiddleware((to) => {
|
|
2
2
|
const { isChannelAllowed } = useChannels()
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
if (!pathMatch) return
|
|
7
|
-
|
|
8
|
-
const channel = pathMatch[1] as 'whatsapp' | 'email'
|
|
4
|
+
const channel = to.meta.channel as 'whatsapp' | 'email' | undefined
|
|
5
|
+
if (!channel) return
|
|
9
6
|
|
|
10
7
|
if (!isChannelAllowed(channel)) {
|
|
11
|
-
return navigateTo('
|
|
8
|
+
return navigateTo({ name: 'message-library' }, { replace: true })
|
|
12
9
|
}
|
|
13
10
|
})
|
package/app/pages/brands.vue
CHANGED
|
@@ -1,191 +1,22 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import type { BrandFromDb } from '@dev.smartpricing/message-composer-utils/types'
|
|
3
|
-
import { formatDate } from '@dev.smartpricing/message-composer-utils/utils'
|
|
4
|
-
import type { TableColumn } from '#ui/types'
|
|
5
|
-
import { useBrandsQuery, useDeleteBrandMutation } from '~/queries'
|
|
6
|
-
import BrandDrawer from '~/components/Brand/BrandDrawer.vue'
|
|
7
|
-
|
|
8
2
|
definePageMeta({
|
|
9
3
|
name: 'brands',
|
|
10
4
|
})
|
|
11
5
|
|
|
12
6
|
const { t } = useI18n()
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
const { data: brands, isPending } = useBrandsQuery()
|
|
16
|
-
const { mutateAsync: deleteBrand } = useDeleteBrandMutation()
|
|
17
|
-
|
|
18
|
-
// Drawer state
|
|
19
|
-
const drawerOpen = ref(false)
|
|
20
|
-
const editingBrandId = ref<string | undefined>(undefined)
|
|
21
|
-
|
|
22
|
-
// Search
|
|
23
|
-
const search = ref('')
|
|
24
|
-
const filteredBrands = computed(() => {
|
|
25
|
-
if (!brands.value?.length) return []
|
|
26
|
-
if (!search.value.trim()) return brands.value
|
|
27
|
-
const query = search.value.toLowerCase()
|
|
28
|
-
return brands.value.filter((b) => b.name.toLowerCase().includes(query))
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
// Table config
|
|
32
|
-
const columns: TableColumn<BrandFromDb>[] = [
|
|
33
|
-
{
|
|
34
|
-
id: 'logo',
|
|
35
|
-
header: '',
|
|
36
|
-
enableSorting: false,
|
|
37
|
-
meta: {
|
|
38
|
-
class: {
|
|
39
|
-
th: 'w-12',
|
|
40
|
-
td: 'w-12',
|
|
41
|
-
},
|
|
42
|
-
},
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
accessorKey: 'name',
|
|
46
|
-
header: (ctx) => getSortableHeader(ctx, t('pages.brands.column_name')),
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
accessorKey: 'created_at',
|
|
50
|
-
header: (ctx) => getSortableHeader(ctx, t('pages.brands.column_created')),
|
|
51
|
-
cell: ({ row }) => formatDate(row.original.created_at),
|
|
52
|
-
},
|
|
53
|
-
{
|
|
54
|
-
id: 'actions',
|
|
55
|
-
enableSorting: false,
|
|
56
|
-
header: '',
|
|
57
|
-
meta: {
|
|
58
|
-
class: {
|
|
59
|
-
th: 'w-[66px]',
|
|
60
|
-
td: 'w-[66px]',
|
|
61
|
-
},
|
|
62
|
-
},
|
|
63
|
-
},
|
|
64
|
-
]
|
|
65
|
-
|
|
66
|
-
const columnPinning = ref({
|
|
67
|
-
right: ['actions'],
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
const sorting = ref([
|
|
71
|
-
{
|
|
72
|
-
id: 'created_at',
|
|
73
|
-
desc: true,
|
|
74
|
-
},
|
|
75
|
-
])
|
|
76
|
-
|
|
77
|
-
function getRowActions(brand: BrandFromDb) {
|
|
78
|
-
return [
|
|
79
|
-
{
|
|
80
|
-
label: t('common.actions.edit'),
|
|
81
|
-
icon: 'ph:pencil-simple-line',
|
|
82
|
-
onSelect: () => openEdit(brand),
|
|
83
|
-
},
|
|
84
|
-
{
|
|
85
|
-
label: t('common.actions.delete'),
|
|
86
|
-
icon: 'ph:trash',
|
|
87
|
-
color: 'error' as const,
|
|
88
|
-
onSelect: () => askDelete(brand),
|
|
89
|
-
},
|
|
90
|
-
]
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function openCreate() {
|
|
94
|
-
editingBrandId.value = undefined
|
|
95
|
-
drawerOpen.value = true
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function openEdit(brand: BrandFromDb) {
|
|
99
|
-
editingBrandId.value = brand.id
|
|
100
|
-
drawerOpen.value = true
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function askDelete(brand: BrandFromDb) {
|
|
104
|
-
confirm({
|
|
105
|
-
title: t('pages.brands.delete_confirm_title'),
|
|
106
|
-
message: t('pages.brands.delete_confirm_message', { name: brand.name }),
|
|
107
|
-
destructive: true,
|
|
108
|
-
confirmProps: {
|
|
109
|
-
label: t('common.actions.delete'),
|
|
110
|
-
},
|
|
111
|
-
cancelProps: {
|
|
112
|
-
label: t('common.actions.cancel'),
|
|
113
|
-
},
|
|
114
|
-
action: async () => {
|
|
115
|
-
await deleteBrand(brand.id)
|
|
116
|
-
useToast().add({
|
|
117
|
-
color: 'success',
|
|
118
|
-
title: t('common.success'),
|
|
119
|
-
description: t('pages.brands.brand_deleted', { name: brand.name }),
|
|
120
|
-
})
|
|
121
|
-
},
|
|
122
|
-
})
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function handleDrawerSaved() {
|
|
126
|
-
drawerOpen.value = false
|
|
127
|
-
}
|
|
7
|
+
const brandListRef = ref<InstanceType<typeof ComposerBrandList>>()
|
|
128
8
|
</script>
|
|
129
9
|
|
|
130
10
|
<template>
|
|
131
11
|
<LayoutBasePage :title="t('pages.brands.title')">
|
|
132
12
|
<template #actions>
|
|
133
|
-
<UButton
|
|
13
|
+
<UButton
|
|
14
|
+
icon="ph:plus"
|
|
15
|
+
:label="t('pages.brands.new_brand')"
|
|
16
|
+
@click="brandListRef?.openCreate()"
|
|
17
|
+
/>
|
|
134
18
|
</template>
|
|
135
19
|
|
|
136
|
-
<
|
|
137
|
-
<div class="flex gap-2">
|
|
138
|
-
<UInput
|
|
139
|
-
v-model="search"
|
|
140
|
-
class="max-w-xs"
|
|
141
|
-
leading-icon="ph:magnifying-glass-bold"
|
|
142
|
-
:placeholder="t('pages.brands.search_placeholder')"
|
|
143
|
-
type="search"
|
|
144
|
-
/>
|
|
145
|
-
</div>
|
|
146
|
-
|
|
147
|
-
<UCard
|
|
148
|
-
:ui="{
|
|
149
|
-
root: 'flex-1 min-h-0 flex flex-col',
|
|
150
|
-
body: 'overflow-auto relative flex-1 p-0 sm:p-0 relative',
|
|
151
|
-
}"
|
|
152
|
-
>
|
|
153
|
-
<UTable
|
|
154
|
-
v-model:column-pinning="columnPinning"
|
|
155
|
-
v-model:sorting="sorting"
|
|
156
|
-
:data="filteredBrands"
|
|
157
|
-
:columns="columns"
|
|
158
|
-
:loading="isPending"
|
|
159
|
-
:empty="t('pages.brands.no_brands')"
|
|
160
|
-
sticky
|
|
161
|
-
class="flex-1 bg-white"
|
|
162
|
-
:ui="{
|
|
163
|
-
tr: 'hover:bg-muted',
|
|
164
|
-
root: 'overflow-visible static',
|
|
165
|
-
}"
|
|
166
|
-
>
|
|
167
|
-
<template #logo-cell="{ row }">
|
|
168
|
-
<img
|
|
169
|
-
v-if="row.original.logo_url"
|
|
170
|
-
:src="row.original.logo_url"
|
|
171
|
-
:alt="row.original.name"
|
|
172
|
-
class="h-8 w-8 rounded-full object-cover border border-neutral-200"
|
|
173
|
-
/>
|
|
174
|
-
<div
|
|
175
|
-
v-else
|
|
176
|
-
class="h-8 w-8 rounded-full bg-neutral-200 flex items-center justify-center text-xs font-medium text-neutral-500"
|
|
177
|
-
>
|
|
178
|
-
{{ row.original.name.charAt(0).toUpperCase() }}
|
|
179
|
-
</div>
|
|
180
|
-
</template>
|
|
181
|
-
|
|
182
|
-
<template #actions-cell="{ row }">
|
|
183
|
-
<SMoreActions :actions="getRowActions(row.original)" />
|
|
184
|
-
</template>
|
|
185
|
-
</UTable>
|
|
186
|
-
</UCard>
|
|
187
|
-
</div>
|
|
188
|
-
|
|
189
|
-
<BrandDrawer v-model="drawerOpen" :brand-id="editingBrandId" @saved="handleDrawerSaved" />
|
|
20
|
+
<ComposerBrandList ref="brandListRef" />
|
|
190
21
|
</LayoutBasePage>
|
|
191
22
|
</template>
|
package/app/pages/images.vue
CHANGED
|
@@ -1,137 +1,13 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
definePageMeta({
|
|
3
|
+
name: 'images',
|
|
4
|
+
})
|
|
5
5
|
|
|
6
6
|
const { t } = useI18n()
|
|
7
|
-
|
|
8
|
-
const { data: media, isPending, refetch } = useMediaQuery()
|
|
9
|
-
const { mutateAsync: uploadToSmartchat } = useUploadToSmartchatMutation()
|
|
10
|
-
const { mutateAsync: deleteMedia } = useDeleteMediaMutation()
|
|
11
|
-
|
|
12
|
-
const uploadingIds = ref<Set<string>>(new Set())
|
|
13
|
-
const deletingIds = ref<Set<string>>(new Set())
|
|
14
|
-
|
|
15
|
-
async function handleUploadToSmartchat(item: MediaFromDb) {
|
|
16
|
-
uploadingIds.value.add(item.id)
|
|
17
|
-
try {
|
|
18
|
-
await uploadToSmartchat(item.id)
|
|
19
|
-
useToast().add({
|
|
20
|
-
color: 'success',
|
|
21
|
-
title: t('common.success'),
|
|
22
|
-
description: t('pages.images.image_synced'),
|
|
23
|
-
})
|
|
24
|
-
refetch()
|
|
25
|
-
} finally {
|
|
26
|
-
uploadingIds.value.delete(item.id)
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
async function handleDelete(item: MediaFromDb) {
|
|
31
|
-
if (!confirm(t('pages.images.delete_confirm', { name: item.file_name }))) return
|
|
32
|
-
|
|
33
|
-
deletingIds.value.add(item.id)
|
|
34
|
-
try {
|
|
35
|
-
await deleteMedia(item.id)
|
|
36
|
-
useToast().add({
|
|
37
|
-
color: 'success',
|
|
38
|
-
title: t('common.success'),
|
|
39
|
-
description: t('pages.images.image_deleted'),
|
|
40
|
-
})
|
|
41
|
-
refetch()
|
|
42
|
-
} finally {
|
|
43
|
-
deletingIds.value.delete(item.id)
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
7
|
</script>
|
|
47
8
|
|
|
48
9
|
<template>
|
|
49
|
-
<LayoutBasePage
|
|
50
|
-
|
|
51
|
-
:test-id="imagesPageTestIds.navbar"
|
|
52
|
-
>
|
|
53
|
-
<div v-if="isPending" class="flex justify-center py-12">
|
|
54
|
-
<UIcon
|
|
55
|
-
name="i-heroicons-arrow-path"
|
|
56
|
-
class="animate-spin h-8 w-8"
|
|
57
|
-
:data-testid="imagesPageTestIds.loadingIcon"
|
|
58
|
-
/>
|
|
59
|
-
</div>
|
|
60
|
-
|
|
61
|
-
<div
|
|
62
|
-
v-else-if="!media || media.length === 0"
|
|
63
|
-
class="text-center py-12 text-gray-500"
|
|
64
|
-
:data-testid="imagesPageTestIds.emptyState"
|
|
65
|
-
>
|
|
66
|
-
{{ t('pages.images.no_images_uploaded') }}
|
|
67
|
-
</div>
|
|
68
|
-
|
|
69
|
-
<div
|
|
70
|
-
v-else
|
|
71
|
-
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4"
|
|
72
|
-
:data-testid="imagesPageTestIds.grid"
|
|
73
|
-
>
|
|
74
|
-
<UCard
|
|
75
|
-
v-for="item in media"
|
|
76
|
-
:key="item.id"
|
|
77
|
-
:data-testid="`${imagesPageTestIds.card}-${item.id}`"
|
|
78
|
-
>
|
|
79
|
-
<div class="space-y-3">
|
|
80
|
-
<img
|
|
81
|
-
:src="item.remote_url"
|
|
82
|
-
:alt="item.file_name"
|
|
83
|
-
class="h-40 w-full object-cover rounded"
|
|
84
|
-
:data-testid="`${imagesPageTestIds.image}-${item.id}`"
|
|
85
|
-
/>
|
|
86
|
-
|
|
87
|
-
<div class="space-y-2">
|
|
88
|
-
<div class="flex items-center justify-between">
|
|
89
|
-
<span
|
|
90
|
-
class="text-sm font-medium truncate"
|
|
91
|
-
:title="item.file_name"
|
|
92
|
-
:data-testid="`${imagesPageTestIds.fileName}-${item.id}`"
|
|
93
|
-
>
|
|
94
|
-
{{ item.file_name }}
|
|
95
|
-
</span>
|
|
96
|
-
<UBadge
|
|
97
|
-
:color="item.smartchat_media_id ? 'success' : 'neutral'"
|
|
98
|
-
variant="subtle"
|
|
99
|
-
size="xs"
|
|
100
|
-
:data-testid="`${imagesPageTestIds.syncBadge}-${item.id}`"
|
|
101
|
-
>
|
|
102
|
-
{{
|
|
103
|
-
item.smartchat_media_id ? t('pages.images.synced') : t('pages.images.not_synced')
|
|
104
|
-
}}
|
|
105
|
-
</UBadge>
|
|
106
|
-
</div>
|
|
107
|
-
|
|
108
|
-
<div class="text-xs text-gray-500 space-y-1">
|
|
109
|
-
<div>{{ t('pages.images.type_label') }} {{ item.file_type }}</div>
|
|
110
|
-
<div>{{ t('pages.images.created_label') }} {{ formatDate(item.created_at) }}</div>
|
|
111
|
-
<div
|
|
112
|
-
v-if="item.smartchat_media_id"
|
|
113
|
-
class="font-mono truncate"
|
|
114
|
-
:title="item.smartchat_media_id"
|
|
115
|
-
>
|
|
116
|
-
{{ t('pages.images.media_id_label') }} {{ item.smartchat_media_id }}
|
|
117
|
-
</div>
|
|
118
|
-
</div>
|
|
119
|
-
|
|
120
|
-
<div v-if="!item.smartchat_media_id" class="flex gap-2 pt-2">
|
|
121
|
-
<UButton
|
|
122
|
-
size="xs"
|
|
123
|
-
color="primary"
|
|
124
|
-
block
|
|
125
|
-
:loading="uploadingIds.has(item.id)"
|
|
126
|
-
:data-testid="`${imagesPageTestIds.syncButton}-${item.id}`"
|
|
127
|
-
@click="handleUploadToSmartchat(item)"
|
|
128
|
-
>
|
|
129
|
-
{{ t('pages.images.sync_to_smartchat') }}
|
|
130
|
-
</UButton>
|
|
131
|
-
</div>
|
|
132
|
-
</div>
|
|
133
|
-
</div>
|
|
134
|
-
</UCard>
|
|
135
|
-
</div>
|
|
10
|
+
<LayoutBasePage :title="t('pages.images.title')" :test-id="imagesPageTestIds.navbar">
|
|
11
|
+
<ComposerImageGallery />
|
|
136
12
|
</LayoutBasePage>
|
|
137
13
|
</template>
|
package/app/pages/index.vue
CHANGED
|
@@ -3,26 +3,15 @@ definePageMeta({
|
|
|
3
3
|
name: 'message-library',
|
|
4
4
|
})
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
useMessageGroupsQuery,
|
|
8
|
-
useDeleteMessageGroupMutation,
|
|
9
|
-
usePublishMessageGroupMutation,
|
|
10
|
-
useUnpublishMessageGroupMutation,
|
|
11
|
-
} from '~/queries'
|
|
12
|
-
import type {
|
|
13
|
-
MessageGroupWithMessageSummary,
|
|
14
|
-
RenderType,
|
|
15
|
-
} from '@dev.smartpricing/message-composer-utils/types'
|
|
16
|
-
import type { MessageActionProperties, MessagesKind } from '@/types/tracking'
|
|
17
|
-
import { ref, watch, computed } from 'vue'
|
|
18
|
-
import { useRouter } from 'vue-router'
|
|
19
|
-
import BaseTable from '@/components/TemplateList/BaseTable.vue'
|
|
6
|
+
import type { RenderType } from '@dev.smartpricing/message-composer-utils/types'
|
|
20
7
|
import CreateTemplateButton from '@/components/CreateTemplateButton.vue'
|
|
8
|
+
import { ref, watch, computed } from 'vue'
|
|
21
9
|
|
|
22
10
|
const { hasWhatsapp, hasEmail, defaultRenderType, isRenderTypeAllowed } = useChannels()
|
|
23
11
|
const { trackEvent } = useTracking()
|
|
12
|
+
const { t } = useI18n()
|
|
24
13
|
|
|
25
|
-
//
|
|
14
|
+
// Render type with session persistence
|
|
26
15
|
const RENDER_TYPE_KEY = 'template_render_type'
|
|
27
16
|
const storedRenderType = sessionStorage.getItem(RENDER_TYPE_KEY) as RenderType | null
|
|
28
17
|
const initialRenderType =
|
|
@@ -31,122 +20,10 @@ const initialRenderType =
|
|
|
31
20
|
: defaultRenderType.value
|
|
32
21
|
const renderType = ref<RenderType>(initialRenderType)
|
|
33
22
|
|
|
34
|
-
// Persist render type to session when it changes
|
|
35
23
|
watch(renderType, (newType) => {
|
|
36
24
|
sessionStorage.setItem(RENDER_TYPE_KEY, newType)
|
|
37
25
|
trackEvent('change_tab', { messages_kind: newType === 'email_v1' ? 'email' : 'whatsapp' })
|
|
38
26
|
})
|
|
39
|
-
const {
|
|
40
|
-
data: messageGroups,
|
|
41
|
-
isPending: isFetching,
|
|
42
|
-
refetch: execute,
|
|
43
|
-
} = useMessageGroupsQuery(computed(() => ({ renderType: renderType.value })))
|
|
44
|
-
|
|
45
|
-
// Manual polling (Pinia Colada doesn't support automatic polling yet)
|
|
46
|
-
const interval = setInterval(() => {
|
|
47
|
-
execute()
|
|
48
|
-
}, 15000)
|
|
49
|
-
|
|
50
|
-
onBeforeUnmount(() => {
|
|
51
|
-
clearInterval(interval)
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
const { confirm } = useConfirm()
|
|
55
|
-
const { mutateAsync: deleteGroup } = useDeleteMessageGroupMutation()
|
|
56
|
-
const { mutateAsync: publishGroup } = usePublishMessageGroupMutation()
|
|
57
|
-
const { mutateAsync: unpublishGroup } = useUnpublishMessageGroupMutation()
|
|
58
|
-
|
|
59
|
-
function getMessageTrackingProps(row: MessageGroupWithMessageSummary): MessageActionProperties {
|
|
60
|
-
return {
|
|
61
|
-
message_name: row.name,
|
|
62
|
-
languages: row.messages.map((m) => m.language),
|
|
63
|
-
message_id: row.id,
|
|
64
|
-
message_kind: (row.messages[0]?.render_type === 'email_v1'
|
|
65
|
-
? 'email'
|
|
66
|
-
: 'whatsapp') as MessagesKind,
|
|
67
|
-
message_category: row.category,
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function askDeleteGroup(row: MessageGroupWithMessageSummary) {
|
|
72
|
-
confirm({
|
|
73
|
-
title: t('dialogs.delete_message_group.title'),
|
|
74
|
-
message: t('dialogs.delete_message_group.message', { name: row.name }),
|
|
75
|
-
destructive: true,
|
|
76
|
-
confirmLabel: t('common.actions.delete'),
|
|
77
|
-
cancelLabel: t('common.actions.cancel'),
|
|
78
|
-
action: async () => {
|
|
79
|
-
await deleteGroup(row.id)
|
|
80
|
-
trackEvent('message_delete', getMessageTrackingProps(row))
|
|
81
|
-
notifyParent('TEMPLATE_DELETED', { id: row.id, name: row.name })
|
|
82
|
-
useToast().add({
|
|
83
|
-
title: t('common.success'),
|
|
84
|
-
description: t('dialogs.delete_message_group.success', { name: row.name }),
|
|
85
|
-
color: 'success',
|
|
86
|
-
})
|
|
87
|
-
execute()
|
|
88
|
-
},
|
|
89
|
-
})
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function askPublishGroup(row: MessageGroupWithMessageSummary) {
|
|
93
|
-
confirm({
|
|
94
|
-
title: t('dialogs.publish_message_group.title'),
|
|
95
|
-
message: t('dialogs.publish_message_group.message', { name: row.name }),
|
|
96
|
-
confirmLabel: t('common.actions.publish'),
|
|
97
|
-
cancelLabel: t('common.actions.cancel'),
|
|
98
|
-
action: async () => {
|
|
99
|
-
await publishGroup(row.id)
|
|
100
|
-
notifyParent('TEMPLATE_PUBLISHED', { id: row.id, name: row.name })
|
|
101
|
-
useToast().add({
|
|
102
|
-
title: t('common.success'),
|
|
103
|
-
description: t('dialogs.publish_message_group.success', { name: row.name }),
|
|
104
|
-
color: 'success',
|
|
105
|
-
})
|
|
106
|
-
execute()
|
|
107
|
-
},
|
|
108
|
-
})
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function askUnpublishGroup(row: MessageGroupWithMessageSummary) {
|
|
112
|
-
confirm({
|
|
113
|
-
title: t('dialogs.unpublish_message_group.title'),
|
|
114
|
-
message: t('dialogs.unpublish_message_group.message', { name: row.name }),
|
|
115
|
-
confirmLabel: t('common.actions.unpublish'),
|
|
116
|
-
cancelLabel: t('common.actions.cancel'),
|
|
117
|
-
action: async () => {
|
|
118
|
-
await unpublishGroup(row.id)
|
|
119
|
-
notifyParent('TEMPLATE_UNPUBLISHED', { id: row.id, name: row.name })
|
|
120
|
-
useToast().add({
|
|
121
|
-
title: t('common.success'),
|
|
122
|
-
description: t('dialogs.unpublish_message_group.success', { name: row.name }),
|
|
123
|
-
color: 'success',
|
|
124
|
-
})
|
|
125
|
-
execute()
|
|
126
|
-
},
|
|
127
|
-
})
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const { duplicateTemplate } = useTemplateOperations({
|
|
131
|
-
onSuccess: () => execute(), // Refresh the list
|
|
132
|
-
})
|
|
133
|
-
|
|
134
|
-
async function handleDuplicate(row: MessageGroupWithMessageSummary) {
|
|
135
|
-
trackEvent('message_duplicate', getMessageTrackingProps(row))
|
|
136
|
-
const result = await duplicateTemplate(row.id.toString(), t('common.copy_of', { name: row.name }))
|
|
137
|
-
if (result) {
|
|
138
|
-
notifyParent('TEMPLATE_DUPLICATED', { id: result.id, sourceId: row.id, name: result.name })
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const router = useRouter()
|
|
143
|
-
function navigate(row: MessageGroupWithMessageSummary) {
|
|
144
|
-
trackEvent('message_edit', getMessageTrackingProps(row))
|
|
145
|
-
const type = row.messages[0]?.render_type === 'email_v1' ? 'email' : 'whatsapp'
|
|
146
|
-
router.push(`/template/${type}/${row.id}`)
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const { t } = useI18n()
|
|
150
27
|
|
|
151
28
|
const tabItems = computed(() => {
|
|
152
29
|
const items = []
|
|
@@ -174,21 +51,12 @@ const tabItems = computed(() => {
|
|
|
174
51
|
:tabs="tabItems.length > 1 ? tabItems : undefined"
|
|
175
52
|
:active-tab="renderType"
|
|
176
53
|
:test-id="homeTestIds.navbar"
|
|
177
|
-
@tab-change="renderType = $event"
|
|
54
|
+
@tab-change="renderType = $event as RenderType"
|
|
178
55
|
>
|
|
179
56
|
<template #actions>
|
|
180
57
|
<CreateTemplateButton />
|
|
181
58
|
</template>
|
|
182
59
|
|
|
183
|
-
<
|
|
184
|
-
:loading="isFetching"
|
|
185
|
-
:message-groups="messageGroups || []"
|
|
186
|
-
:render-type="renderType"
|
|
187
|
-
@edit="navigate"
|
|
188
|
-
@delete="askDeleteGroup"
|
|
189
|
-
@duplicate="handleDuplicate"
|
|
190
|
-
@publish="askPublishGroup"
|
|
191
|
-
@unpublish="askUnpublishGroup"
|
|
192
|
-
/>
|
|
60
|
+
<ComposerMessageLibrary v-model:render-type="renderType" />
|
|
193
61
|
</LayoutBasePage>
|
|
194
62
|
</template>
|