@dev.smartpricing/message-composer-layer 1.0.13 → 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.
@@ -0,0 +1,95 @@
1
+ <script setup lang="ts">
2
+ import EmailEditor from '@/components/Email/Editor/Index.vue'
3
+ import type { PostEntity, EmailMessage } from '@dev.smartpricing/message-composer-utils/types'
4
+
5
+ const props = defineProps<{
6
+ mode: 'create' | 'edit'
7
+ }>()
8
+
9
+ const emit = defineEmits<{
10
+ 'block-added': [payload: { type: string; adding_mode: string }]
11
+ 'block-deleted': [payload: { type: string }]
12
+ 'block-duplicated': [payload: { type: string }]
13
+ 'block-sorted': [payload: { type: string; before_sort_position: number; new_position: number }]
14
+ 'widget-tab-changed': [tab: string]
15
+ 'test-sent': [payload: { to_recipients: number }, lang?: string]
16
+ }>()
17
+
18
+ const store = useEmailComposerStore()
19
+ const emailEditorStore = useEmailEditorStore()
20
+ const adminStore = useAdminStore()
21
+
22
+ const testIds = computed(() =>
23
+ props.mode === 'create' ? templateEmailCreateTestIds : templateEmailEditTestIds,
24
+ )
25
+ </script>
26
+
27
+ <template>
28
+ <UCard
29
+ v-if="store.currentMessage"
30
+ :ui="{
31
+ root: 'flex flex-col flex-1 min-h-0',
32
+ body: 'flex-1 overflow-hidden sm:px-0 px-0 py-0 sm:py-0 min-h-0',
33
+ header: 'sm:px-4 px-4 py-4 sm:py-4',
34
+ }"
35
+ :data-testid="testIds.contentCard"
36
+ >
37
+ <template #header>
38
+ <div class="grid grid-cols-3 gap-2 items-center">
39
+ <UFieldGroup>
40
+ <UBadge variant="outline" color="neutral" size="lg">{{
41
+ $t(`pages.email.${mode === 'create' ? 'create' : 'edit'}_message_group.name`)
42
+ }}</UBadge>
43
+ <UInput
44
+ v-model="store.name"
45
+ :placeholder="
46
+ $t(
47
+ `pages.email.${mode === 'create' ? 'create' : 'edit'}_message_group.name_placeholder`,
48
+ )
49
+ "
50
+ :data-testid="testIds.nameInput"
51
+ />
52
+ </UFieldGroup>
53
+
54
+ <UFieldGroup v-if="store.currentMessage">
55
+ <UBadge variant="outline" color="neutral" size="lg">{{
56
+ $t(`pages.email.${mode === 'create' ? 'create' : 'edit'}_message_group.subject`)
57
+ }}</UBadge>
58
+ <UInput
59
+ v-model="(store.messages[store.language] as PostEntity<EmailMessage>).body.subject"
60
+ :placeholder="
61
+ $t(
62
+ `pages.email.${mode === 'create' ? 'create' : 'edit'}_message_group.subject_placeholder`,
63
+ )
64
+ "
65
+ :data-testid="testIds.subjectInput"
66
+ />
67
+ </UFieldGroup>
68
+
69
+ <USwitch
70
+ v-if="mode === 'create' && adminStore.isAdminModeActive"
71
+ :label="$t('common.internal')"
72
+ v-model="store.isInternal"
73
+ color="info"
74
+ :data-testid="testIds.internalSwitch"
75
+ />
76
+ </div>
77
+ </template>
78
+
79
+ <div class="flex flex-col h-full overflow-hidden">
80
+ <template v-if="store.currentMessage" :key="store.language">
81
+ <div class="flex-1 overflow-hidden">
82
+ <EmailEditor
83
+ :message="store.currentMessage as PostEntity<EmailMessage>"
84
+ @block-added="emit('block-added', $event)"
85
+ @block-deleted="emit('block-deleted', $event)"
86
+ @block-duplicated="emit('block-duplicated', $event)"
87
+ @block-sorted="emit('block-sorted', $event)"
88
+ @widget-tab-changed="emit('widget-tab-changed', $event)"
89
+ @test-sent="(payload, lang) => emit('test-sent', payload, lang)"
90
+ />
91
+ </div>
92
+ </template>
93
+ </div>
94
+ </UCard>
95
+ </template>
@@ -0,0 +1,70 @@
1
+ <script setup lang="ts">
2
+ import TranslationEditor from '@/components/Email/TranslationEditor.vue'
3
+ import LanguageList from '@/components/Common/LanguageList.vue'
4
+ import type { EmailMessage, PostEntity } from '@dev.smartpricing/message-composer-utils/types'
5
+
6
+ const props = defineProps<{
7
+ mode: 'create' | 'edit'
8
+ }>()
9
+
10
+ const emit = defineEmits<{
11
+ 'add-language': [lang: string]
12
+ 'remove-language': [lang: string]
13
+ 'auto-translate-tracked': [lang: string]
14
+ preview: [lang: string]
15
+ 'filters-changed': [
16
+ payload: {
17
+ language: string
18
+ search_term: string
19
+ show_only_missing: boolean
20
+ show_only_changes: boolean
21
+ },
22
+ ]
23
+ }>()
24
+
25
+ const store = useEmailComposerStore()
26
+
27
+ const testIds = computed(() =>
28
+ props.mode === 'create' ? templateEmailCreateTestIds : templateEmailEditTestIds,
29
+ )
30
+ </script>
31
+
32
+ <template>
33
+ <UCard
34
+ v-if="store.currentMessage"
35
+ :ui="{
36
+ root: 'flex flex-col h-full',
37
+ body: 'flex-1 overflow-hidden sm:px-0 px-0 py-0 sm:py-0',
38
+ header: 'sm:px-4 px-4 py-4 sm:py-4',
39
+ }"
40
+ :data-testid="testIds.translationsCard"
41
+ >
42
+ <div class="grid grid-cols-[auto_1fr] divide-x divide-muted overflow-auto max-h-full h-full">
43
+ <div class="overflow-y-auto">
44
+ <LanguageList
45
+ v-model:messages="store.messages"
46
+ v-model:language="store.targetLanguage"
47
+ render-type="email_v1"
48
+ :main-language-id="store.language"
49
+ :message-group-id="store.messageGroupId ?? undefined"
50
+ @add-language="emit('add-language', $event)"
51
+ @remove-language="emit('remove-language', $event)"
52
+ />
53
+ </div>
54
+ <div class="overflow-y-auto" v-if="store.currentMessage && store.targetMessage">
55
+ <TranslationEditor
56
+ :base-message="store.currentMessage as PostEntity<EmailMessage>"
57
+ v-model="store.messages[store.targetLanguage] as PostEntity<EmailMessage>"
58
+ :messages="store.messages as Record<string, PostEntity<EmailMessage>>"
59
+ :current-language="store.targetLanguage"
60
+ :base-language="store.language"
61
+ :message-group-id="store.messageGroupId ?? undefined"
62
+ :key="store.targetLanguage"
63
+ @auto-translate-tracked="emit('auto-translate-tracked', $event)"
64
+ @preview="emit('preview', $event)"
65
+ @filters-changed="emit('filters-changed', $event)"
66
+ />
67
+ </div>
68
+ </div>
69
+ </UCard>
70
+ </template>
@@ -0,0 +1,141 @@
1
+ <script setup lang="ts">
2
+ import type { MediaFromDb } from '@dev.smartpricing/message-composer-utils/types'
3
+ import { useMediaQuery, useUploadToSmartchatMutation, useDeleteMediaMutation } from '~/queries'
4
+ import { formatDate } from '@dev.smartpricing/message-composer-utils/utils'
5
+
6
+ const emit = defineEmits<{
7
+ synced: [item: MediaFromDb]
8
+ deleted: [item: MediaFromDb]
9
+ }>()
10
+
11
+ const { t } = useI18n()
12
+
13
+ const { data: media, isPending, refetch } = useMediaQuery()
14
+ const { mutateAsync: uploadToSmartchat } = useUploadToSmartchatMutation()
15
+ const { mutateAsync: deleteMedia } = useDeleteMediaMutation()
16
+
17
+ const uploadingIds = ref<Set<string>>(new Set())
18
+ const deletingIds = ref<Set<string>>(new Set())
19
+
20
+ async function handleUploadToSmartchat(item: MediaFromDb) {
21
+ uploadingIds.value.add(item.id)
22
+ try {
23
+ await uploadToSmartchat(item.id)
24
+ useToast().add({
25
+ color: 'success',
26
+ title: t('common.success'),
27
+ description: t('pages.images.image_synced'),
28
+ })
29
+ emit('synced', item)
30
+ refetch()
31
+ } finally {
32
+ uploadingIds.value.delete(item.id)
33
+ }
34
+ }
35
+
36
+ async function handleDelete(item: MediaFromDb) {
37
+ if (!confirm(t('pages.images.delete_confirm', { name: item.file_name }))) return
38
+
39
+ deletingIds.value.add(item.id)
40
+ try {
41
+ await deleteMedia(item.id)
42
+ useToast().add({
43
+ color: 'success',
44
+ title: t('common.success'),
45
+ description: t('pages.images.image_deleted'),
46
+ })
47
+ emit('deleted', item)
48
+ refetch()
49
+ } finally {
50
+ deletingIds.value.delete(item.id)
51
+ }
52
+ }
53
+
54
+ defineExpose({ refetch })
55
+ </script>
56
+
57
+ <template>
58
+ <div v-if="isPending" class="flex justify-center py-12">
59
+ <UIcon
60
+ name="i-heroicons-arrow-path"
61
+ class="animate-spin h-8 w-8"
62
+ :data-testid="imagesPageTestIds.loadingIcon"
63
+ />
64
+ </div>
65
+
66
+ <div
67
+ v-else-if="!media || media.length === 0"
68
+ class="text-center py-12 text-gray-500"
69
+ :data-testid="imagesPageTestIds.emptyState"
70
+ >
71
+ {{ t('pages.images.no_images_uploaded') }}
72
+ </div>
73
+
74
+ <div
75
+ v-else
76
+ class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4"
77
+ :data-testid="imagesPageTestIds.grid"
78
+ >
79
+ <UCard
80
+ v-for="item in media"
81
+ :key="item.id"
82
+ :data-testid="`${imagesPageTestIds.card}-${item.id}`"
83
+ >
84
+ <div class="space-y-3">
85
+ <img
86
+ :src="item.remote_url"
87
+ :alt="item.file_name"
88
+ class="h-40 w-full object-cover rounded"
89
+ :data-testid="`${imagesPageTestIds.image}-${item.id}`"
90
+ />
91
+
92
+ <div class="space-y-2">
93
+ <div class="flex items-center justify-between">
94
+ <span
95
+ class="text-sm font-medium truncate"
96
+ :title="item.file_name"
97
+ :data-testid="`${imagesPageTestIds.fileName}-${item.id}`"
98
+ >
99
+ {{ item.file_name }}
100
+ </span>
101
+ <UBadge
102
+ :color="item.smartchat_media_id ? 'success' : 'neutral'"
103
+ variant="subtle"
104
+ size="xs"
105
+ :data-testid="`${imagesPageTestIds.syncBadge}-${item.id}`"
106
+ >
107
+ {{
108
+ item.smartchat_media_id ? t('pages.images.synced') : t('pages.images.not_synced')
109
+ }}
110
+ </UBadge>
111
+ </div>
112
+
113
+ <div class="text-xs text-gray-500 space-y-1">
114
+ <div>{{ t('pages.images.type_label') }} {{ item.file_type }}</div>
115
+ <div>{{ t('pages.images.created_label') }} {{ formatDate(item.created_at) }}</div>
116
+ <div
117
+ v-if="item.smartchat_media_id"
118
+ class="font-mono truncate"
119
+ :title="item.smartchat_media_id"
120
+ >
121
+ {{ t('pages.images.media_id_label') }} {{ item.smartchat_media_id }}
122
+ </div>
123
+ </div>
124
+
125
+ <div v-if="!item.smartchat_media_id" class="flex gap-2 pt-2">
126
+ <UButton
127
+ size="xs"
128
+ color="primary"
129
+ block
130
+ :loading="uploadingIds.has(item.id)"
131
+ :data-testid="`${imagesPageTestIds.syncButton}-${item.id}`"
132
+ @click="handleUploadToSmartchat(item)"
133
+ >
134
+ {{ t('pages.images.sync_to_smartchat') }}
135
+ </UButton>
136
+ </div>
137
+ </div>
138
+ </div>
139
+ </UCard>
140
+ </div>
141
+ </template>
@@ -0,0 +1,155 @@
1
+ <script setup lang="ts">
2
+ import {
3
+ useMessageGroupsQuery,
4
+ useDeleteMessageGroupMutation,
5
+ usePublishMessageGroupMutation,
6
+ useUnpublishMessageGroupMutation,
7
+ } from '~/queries'
8
+ import type {
9
+ MessageGroupWithMessageSummary,
10
+ RenderType,
11
+ } from '@dev.smartpricing/message-composer-utils/types'
12
+ import type { MessageActionProperties, MessagesKind } from '@/types/tracking'
13
+ import { computed } from 'vue'
14
+ import BaseTable from '@/components/TemplateList/BaseTable.vue'
15
+
16
+ const renderType = defineModel<RenderType>('renderType', { required: true })
17
+
18
+ const emit = defineEmits<{
19
+ edit: [row: MessageGroupWithMessageSummary]
20
+ deleted: [row: MessageGroupWithMessageSummary]
21
+ duplicated: [result: { id: string | number; sourceId: string | number; name: string }]
22
+ published: [row: MessageGroupWithMessageSummary]
23
+ unpublished: [row: MessageGroupWithMessageSummary]
24
+ }>()
25
+
26
+ const { trackEvent } = useTracking()
27
+ const { t } = useI18n()
28
+
29
+ // Query
30
+ const {
31
+ data: messageGroups,
32
+ isPending: isFetching,
33
+ refetch: execute,
34
+ } = useMessageGroupsQuery(computed(() => ({ renderType: renderType.value })))
35
+
36
+ // Polling
37
+ const interval = setInterval(() => execute(), 15000)
38
+ onBeforeUnmount(() => clearInterval(interval))
39
+
40
+ // Mutations
41
+ const { confirm } = useConfirm()
42
+ const { mutateAsync: deleteGroup } = useDeleteMessageGroupMutation()
43
+ const { mutateAsync: publishGroup } = usePublishMessageGroupMutation()
44
+ const { mutateAsync: unpublishGroup } = useUnpublishMessageGroupMutation()
45
+
46
+ function getMessageTrackingProps(row: MessageGroupWithMessageSummary): MessageActionProperties {
47
+ return {
48
+ message_name: row.name,
49
+ languages: row.messages.map((m) => m.language),
50
+ message_id: row.id,
51
+ message_kind: (row.messages[0]?.render_type === 'email_v1'
52
+ ? 'email'
53
+ : 'whatsapp') as MessagesKind,
54
+ message_category: row.category,
55
+ }
56
+ }
57
+
58
+ function askDeleteGroup(row: MessageGroupWithMessageSummary) {
59
+ confirm({
60
+ title: t('dialogs.delete_message_group.title'),
61
+ message: t('dialogs.delete_message_group.message', { name: row.name }),
62
+ destructive: true,
63
+ confirmLabel: t('common.actions.delete'),
64
+ cancelLabel: t('common.actions.cancel'),
65
+ action: async () => {
66
+ await deleteGroup(row.id)
67
+ trackEvent('message_delete', getMessageTrackingProps(row))
68
+ notifyParent('TEMPLATE_DELETED', { id: row.id, name: row.name })
69
+ useToast().add({
70
+ title: t('common.success'),
71
+ description: t('dialogs.delete_message_group.success', { name: row.name }),
72
+ color: 'success',
73
+ })
74
+ emit('deleted', row)
75
+ execute()
76
+ },
77
+ })
78
+ }
79
+
80
+ function askPublishGroup(row: MessageGroupWithMessageSummary) {
81
+ confirm({
82
+ title: t('dialogs.publish_message_group.title'),
83
+ message: t('dialogs.publish_message_group.message', { name: row.name }),
84
+ confirmLabel: t('common.actions.publish'),
85
+ cancelLabel: t('common.actions.cancel'),
86
+ action: async () => {
87
+ await publishGroup(row.id)
88
+ notifyParent('TEMPLATE_PUBLISHED', { id: row.id, name: row.name })
89
+ useToast().add({
90
+ title: t('common.success'),
91
+ description: t('dialogs.publish_message_group.success', { name: row.name }),
92
+ color: 'success',
93
+ })
94
+ emit('published', row)
95
+ execute()
96
+ },
97
+ })
98
+ }
99
+
100
+ function askUnpublishGroup(row: MessageGroupWithMessageSummary) {
101
+ confirm({
102
+ title: t('dialogs.unpublish_message_group.title'),
103
+ message: t('dialogs.unpublish_message_group.message', { name: row.name }),
104
+ confirmLabel: t('common.actions.unpublish'),
105
+ cancelLabel: t('common.actions.cancel'),
106
+ action: async () => {
107
+ await unpublishGroup(row.id)
108
+ notifyParent('TEMPLATE_UNPUBLISHED', { id: row.id, name: row.name })
109
+ useToast().add({
110
+ title: t('common.success'),
111
+ description: t('dialogs.unpublish_message_group.success', { name: row.name }),
112
+ color: 'success',
113
+ })
114
+ emit('unpublished', row)
115
+ execute()
116
+ },
117
+ })
118
+ }
119
+
120
+ const { duplicateTemplate } = useTemplateOperations({
121
+ onSuccess: () => execute(),
122
+ })
123
+
124
+ async function handleDuplicate(row: MessageGroupWithMessageSummary) {
125
+ trackEvent('message_duplicate', getMessageTrackingProps(row))
126
+ const result = await duplicateTemplate(row.id.toString(), t('common.copy_of', { name: row.name }))
127
+ if (result) {
128
+ notifyParent('TEMPLATE_DUPLICATED', { id: result.id, sourceId: row.id, name: result.name })
129
+ emit('duplicated', { id: result.id, sourceId: row.id, name: result.name })
130
+ }
131
+ }
132
+
133
+ const router = useRouter()
134
+ function navigate(row: MessageGroupWithMessageSummary) {
135
+ trackEvent('message_edit', getMessageTrackingProps(row))
136
+ emit('edit', row)
137
+ const type = row.messages[0]?.render_type === 'email_v1' ? 'email' : 'whatsapp'
138
+ router.push({ name: `${type}-message-edit`, params: { id: row.id } })
139
+ }
140
+
141
+ defineExpose({ execute })
142
+ </script>
143
+
144
+ <template>
145
+ <BaseTable
146
+ :loading="isFetching"
147
+ :message-groups="messageGroups || []"
148
+ :render-type="renderType"
149
+ @edit="navigate"
150
+ @delete="askDeleteGroup"
151
+ @duplicate="handleDuplicate"
152
+ @publish="askPublishGroup"
153
+ @unpublish="askUnpublishGroup"
154
+ />
155
+ </template>