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