@dev.smartpricing/message-composer-layer 1.0.13 → 1.0.15

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.
@@ -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 { confirm } = useConfirm()
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 = useTemplateRef('brandListRef')
128
8
  </script>
129
9
 
130
10
  <template>
131
11
  <LayoutBasePage :title="t('pages.brands.title')">
132
12
  <template #actions>
133
- <UButton icon="ph:plus" :label="t('pages.brands.new_brand')" @click="openCreate" />
13
+ <UButton
14
+ icon="ph:plus"
15
+ :label="t('pages.brands.new_brand')"
16
+ @click="brandListRef?.openCreate()"
17
+ />
134
18
  </template>
135
19
 
136
- <div class="flex-1 space-y-2.5 relative min-h-0 flex flex-col">
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>
@@ -3,136 +3,11 @@ definePageMeta({
3
3
  name: 'images',
4
4
  })
5
5
 
6
- import type { MediaFromDb } from '@dev.smartpricing/message-composer-utils/types'
7
- import { useMediaQuery, useUploadToSmartchatMutation, useDeleteMediaMutation } from '~/queries'
8
- import { formatDate } from '@dev.smartpricing/message-composer-utils/utils'
9
-
10
6
  const { t } = useI18n()
11
-
12
- const { data: media, isPending, refetch } = useMediaQuery()
13
- const { mutateAsync: uploadToSmartchat } = useUploadToSmartchatMutation()
14
- const { mutateAsync: deleteMedia } = useDeleteMediaMutation()
15
-
16
- const uploadingIds = ref<Set<string>>(new Set())
17
- const deletingIds = ref<Set<string>>(new Set())
18
-
19
- async function handleUploadToSmartchat(item: MediaFromDb) {
20
- uploadingIds.value.add(item.id)
21
- try {
22
- await uploadToSmartchat(item.id)
23
- useToast().add({
24
- color: 'success',
25
- title: t('common.success'),
26
- description: t('pages.images.image_synced'),
27
- })
28
- refetch()
29
- } finally {
30
- uploadingIds.value.delete(item.id)
31
- }
32
- }
33
-
34
- async function handleDelete(item: MediaFromDb) {
35
- if (!confirm(t('pages.images.delete_confirm', { name: item.file_name }))) return
36
-
37
- deletingIds.value.add(item.id)
38
- try {
39
- await deleteMedia(item.id)
40
- useToast().add({
41
- color: 'success',
42
- title: t('common.success'),
43
- description: t('pages.images.image_deleted'),
44
- })
45
- refetch()
46
- } finally {
47
- deletingIds.value.delete(item.id)
48
- }
49
- }
50
7
  </script>
51
8
 
52
9
  <template>
53
10
  <LayoutBasePage :title="t('pages.images.title')" :test-id="imagesPageTestIds.navbar">
54
- <div v-if="isPending" class="flex justify-center py-12">
55
- <UIcon
56
- name="i-heroicons-arrow-path"
57
- class="animate-spin h-8 w-8"
58
- :data-testid="imagesPageTestIds.loadingIcon"
59
- />
60
- </div>
61
-
62
- <div
63
- v-else-if="!media || media.length === 0"
64
- class="text-center py-12 text-gray-500"
65
- :data-testid="imagesPageTestIds.emptyState"
66
- >
67
- {{ t('pages.images.no_images_uploaded') }}
68
- </div>
69
-
70
- <div
71
- v-else
72
- class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4"
73
- :data-testid="imagesPageTestIds.grid"
74
- >
75
- <UCard
76
- v-for="item in media"
77
- :key="item.id"
78
- :data-testid="`${imagesPageTestIds.card}-${item.id}`"
79
- >
80
- <div class="space-y-3">
81
- <img
82
- :src="item.remote_url"
83
- :alt="item.file_name"
84
- class="h-40 w-full object-cover rounded"
85
- :data-testid="`${imagesPageTestIds.image}-${item.id}`"
86
- />
87
-
88
- <div class="space-y-2">
89
- <div class="flex items-center justify-between">
90
- <span
91
- class="text-sm font-medium truncate"
92
- :title="item.file_name"
93
- :data-testid="`${imagesPageTestIds.fileName}-${item.id}`"
94
- >
95
- {{ item.file_name }}
96
- </span>
97
- <UBadge
98
- :color="item.smartchat_media_id ? 'success' : 'neutral'"
99
- variant="subtle"
100
- size="xs"
101
- :data-testid="`${imagesPageTestIds.syncBadge}-${item.id}`"
102
- >
103
- {{
104
- item.smartchat_media_id ? t('pages.images.synced') : t('pages.images.not_synced')
105
- }}
106
- </UBadge>
107
- </div>
108
-
109
- <div class="text-xs text-gray-500 space-y-1">
110
- <div>{{ t('pages.images.type_label') }} {{ item.file_type }}</div>
111
- <div>{{ t('pages.images.created_label') }} {{ formatDate(item.created_at) }}</div>
112
- <div
113
- v-if="item.smartchat_media_id"
114
- class="font-mono truncate"
115
- :title="item.smartchat_media_id"
116
- >
117
- {{ t('pages.images.media_id_label') }} {{ item.smartchat_media_id }}
118
- </div>
119
- </div>
120
-
121
- <div v-if="!item.smartchat_media_id" class="flex gap-2 pt-2">
122
- <UButton
123
- size="xs"
124
- color="primary"
125
- block
126
- :loading="uploadingIds.has(item.id)"
127
- :data-testid="`${imagesPageTestIds.syncButton}-${item.id}`"
128
- @click="handleUploadToSmartchat(item)"
129
- >
130
- {{ t('pages.images.sync_to_smartchat') }}
131
- </UButton>
132
- </div>
133
- </div>
134
- </div>
135
- </UCard>
136
- </div>
11
+ <ComposerImageGallery />
137
12
  </LayoutBasePage>
138
13
  </template>
@@ -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
- // Load render type from session or default to first allowed
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({ name: `${type}-message-edit`, params: { id: 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
- <BaseTable
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>