@dev.smartpricing/message-composer-layer 4.3.2-chat.0 → 4.3.2-fix.1
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/client.ts +3 -1
- package/app/api/dynamicValues.ts +2 -5
- package/app/api/messageGroups.ts +12 -23
- package/app/components/Common/MessageGroupSelect.vue +8 -109
- package/app/components/Composer/MessageLibrary.vue +7 -17
- package/app/components/Editor/RichTextEditor.vue +8 -4
- package/app/components/TemplateList/BaseTable.vue +108 -156
- package/app/components/Whatsapp/Editor/Header.vue +1 -4
- package/app/components/Whatsapp/Preview.vue +4 -2
- package/app/components/Whatsapp/Translator/Header.vue +7 -6
- package/app/composables/composerContext.ts +2 -4
- package/app/composables/useTemplateOperations.ts +3 -13
- package/app/layouts/default.vue +0 -1
- package/app/queries/dynamicValues.ts +2 -8
- package/app/queries/messageGroups.ts +16 -18
- package/app/stores/appContext.ts +6 -12
- package/app/types/tracking.ts +0 -4
- package/app/utils/testIds.const.ts +0 -6
- package/i18n/locales/de.ts +2 -18
- package/i18n/locales/en.ts +2 -18
- package/i18n/locales/es.ts +2 -18
- package/i18n/locales/fr.ts +2 -18
- package/i18n/locales/it.ts +2 -18
- package/nuxt.config.ts +3 -4
- package/package.json +4 -3
- package/app/components/Common/AppContextErrors.vue +0 -30
- package/app/composables/useMessageGroupList.ts +0 -186
- package/app/composables/usePagedMessageGroups.ts +0 -66
|
@@ -1,36 +1,16 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import {
|
|
3
3
|
MESSAGE_GROUP_CATEGORIES,
|
|
4
|
-
MESSAGE_GROUP_LIST_VISIBILITY,
|
|
5
|
-
MESSAGE_GROUP_PRODUCTS,
|
|
6
4
|
MESSAGE_STATUS,
|
|
7
|
-
type MessageGroupListSort,
|
|
8
5
|
type MessageGroupWithMessageSummary,
|
|
9
|
-
type PaginationMeta,
|
|
10
6
|
type RenderType,
|
|
11
7
|
} from '@dev.smartpricing/message-composer-utils/types'
|
|
12
8
|
import StatusCell from '@/components/TemplateList/StatusCell.vue'
|
|
13
9
|
import { useI18n } from 'vue-i18n'
|
|
14
|
-
import {
|
|
10
|
+
import { ref, watch } from 'vue'
|
|
15
11
|
import { formatDate } from '@dev.smartpricing/message-composer-utils/utils'
|
|
16
12
|
import type { TableColumn } from '#ui/types'
|
|
17
13
|
import type { TrackingEventMap } from '@/types/tracking'
|
|
18
|
-
import {
|
|
19
|
-
ALL_FILTER,
|
|
20
|
-
type MessageGroupListFilters,
|
|
21
|
-
type MessageGroupListSorting,
|
|
22
|
-
} from '@/composables/useMessageGroupList'
|
|
23
|
-
|
|
24
|
-
const props = defineProps<{
|
|
25
|
-
messageGroups: MessageGroupWithMessageSummary[]
|
|
26
|
-
/** Envelope meta of the current page; undefined until the first page arrives. */
|
|
27
|
-
meta?: PaginationMeta
|
|
28
|
-
loading: boolean
|
|
29
|
-
renderType: RenderType
|
|
30
|
-
filters: MessageGroupListFilters
|
|
31
|
-
sorting: MessageGroupListSorting
|
|
32
|
-
page: number
|
|
33
|
-
}>()
|
|
34
14
|
|
|
35
15
|
const emit = defineEmits<{
|
|
36
16
|
edit: [MessageGroupWithMessageSummary]
|
|
@@ -38,9 +18,6 @@ const emit = defineEmits<{
|
|
|
38
18
|
duplicate: [MessageGroupWithMessageSummary]
|
|
39
19
|
publish: [MessageGroupWithMessageSummary]
|
|
40
20
|
unpublish: [MessageGroupWithMessageSummary]
|
|
41
|
-
'update:filters': [Partial<MessageGroupListFilters>]
|
|
42
|
-
'update:sorting': [MessageGroupListSorting]
|
|
43
|
-
'update:page': [number]
|
|
44
21
|
}>()
|
|
45
22
|
|
|
46
23
|
const appContext = useAppContextStore()
|
|
@@ -109,40 +86,49 @@ function rowActions(row: MessageGroupWithMessageSummary) {
|
|
|
109
86
|
]
|
|
110
87
|
}
|
|
111
88
|
|
|
112
|
-
// Column ids of sortable columns are the server's `sort` values (ADR 0012);
|
|
113
|
-
// preview and status are aggregates and stay unsortable.
|
|
114
89
|
const columns: TableColumn<MessageGroupWithMessageSummary>[] = [
|
|
115
90
|
{
|
|
116
|
-
id: 'name'
|
|
91
|
+
id: 'name',
|
|
117
92
|
accessorKey: 'name',
|
|
118
93
|
header: (ctx) => getSortableHeader(ctx, t('pages.message_library.headers.name')),
|
|
119
94
|
},
|
|
120
95
|
{
|
|
121
|
-
id: 'category'
|
|
96
|
+
id: 'category',
|
|
122
97
|
accessorKey: 'category',
|
|
123
98
|
header: (ctx) => getSortableHeader(ctx, t('pages.message_library.headers.category')),
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
enableSorting: false,
|
|
129
|
-
header: () => t('pages.message_library.headers.product'),
|
|
99
|
+
filterFn: (row, columnId, filterValue) => {
|
|
100
|
+
if (filterValue === 'all') return true
|
|
101
|
+
return row.original.category.toLowerCase().includes(filterValue.toLowerCase())
|
|
102
|
+
},
|
|
130
103
|
},
|
|
131
104
|
{
|
|
132
105
|
id: 'preview',
|
|
133
106
|
accessorFn: (row) => getLocalizedMessage(row)?.preview,
|
|
134
|
-
|
|
135
|
-
header: () => t('pages.message_library.headers.preview'),
|
|
107
|
+
header: (ctx) => getSortableHeader(ctx, t('pages.message_library.headers.preview')),
|
|
136
108
|
},
|
|
137
109
|
{
|
|
138
110
|
id: 'status',
|
|
139
111
|
accessorFn: (row) => row.messages.map((m) => m.status).join(', '),
|
|
140
|
-
|
|
141
|
-
|
|
112
|
+
header: (ctx) => getSortableHeader(ctx, t('pages.message_library.headers.status')),
|
|
113
|
+
filterFn: (row, columnId, filterValue) => {
|
|
114
|
+
if (filterValue === 'all') return true
|
|
115
|
+
return row.original.messages.some((m) => m.status === filterValue)
|
|
116
|
+
},
|
|
142
117
|
},
|
|
118
|
+
// {
|
|
119
|
+
// id: 'tags',
|
|
120
|
+
// accessorFn: (row) => row.tags?.join(', ') || '',
|
|
121
|
+
// header: (ctx) => getSortableHeader(ctx, t('pages.message_library.headers.tags')),
|
|
122
|
+
// filterFn: (row, _columnId, filterValue: string[]) => {
|
|
123
|
+
// if (!filterValue || filterValue.length === 0) return true
|
|
124
|
+
// const rowTags = row.original.tags || []
|
|
125
|
+
// return filterValue.some((tag) => rowTags.includes(tag))
|
|
126
|
+
// },
|
|
127
|
+
// },
|
|
143
128
|
{
|
|
144
|
-
id: 'updated_at'
|
|
129
|
+
id: 'updated_at',
|
|
145
130
|
accessorFn: (row) => formatDate(row.updated_at),
|
|
131
|
+
// header: () => t('pages.message_library.headers.updated_at'),
|
|
146
132
|
header: (ctx) => getSortableHeader(ctx, t('pages.message_library.headers.updated_at')),
|
|
147
133
|
},
|
|
148
134
|
{
|
|
@@ -158,70 +144,77 @@ const columns: TableColumn<MessageGroupWithMessageSummary>[] = [
|
|
|
158
144
|
},
|
|
159
145
|
]
|
|
160
146
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
147
|
+
const sorting = ref([
|
|
148
|
+
{
|
|
149
|
+
id: 'updated_at',
|
|
150
|
+
desc: true,
|
|
151
|
+
},
|
|
152
|
+
])
|
|
165
153
|
|
|
166
|
-
const
|
|
154
|
+
const props = defineProps<{
|
|
155
|
+
messageGroups: MessageGroupWithMessageSummary[]
|
|
156
|
+
loading: boolean
|
|
157
|
+
renderType: RenderType
|
|
158
|
+
}>()
|
|
167
159
|
|
|
168
|
-
|
|
169
|
-
()
|
|
170
|
-
(
|
|
171
|
-
|
|
172
|
-
},
|
|
173
|
-
)
|
|
160
|
+
const filteredMessageGroups = computed(() => {
|
|
161
|
+
if (appContext.isAdmin) return props.messageGroups
|
|
162
|
+
return props.messageGroups.filter((mg) => mg.visibility !== 'internal')
|
|
163
|
+
})
|
|
174
164
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
sort_column: next.sort,
|
|
187
|
-
})
|
|
165
|
+
const categoryItems = computed(() => {
|
|
166
|
+
return [
|
|
167
|
+
{
|
|
168
|
+
label: t('common.all'),
|
|
169
|
+
value: 'all',
|
|
170
|
+
},
|
|
171
|
+
...MESSAGE_GROUP_CATEGORIES.map((category) => ({
|
|
172
|
+
label: t(`categories.${category}`),
|
|
173
|
+
value: category,
|
|
174
|
+
})),
|
|
175
|
+
]
|
|
188
176
|
})
|
|
189
177
|
|
|
190
|
-
|
|
191
|
-
function toSelectItems<T extends string>(
|
|
192
|
-
values: readonly T[],
|
|
193
|
-
keyPrefix: string,
|
|
194
|
-
allLabel: string,
|
|
195
|
-
) {
|
|
178
|
+
const statusItems = computed(() => {
|
|
196
179
|
return [
|
|
197
|
-
{
|
|
198
|
-
|
|
180
|
+
{
|
|
181
|
+
label: t('common.all'),
|
|
182
|
+
value: 'all',
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
...MESSAGE_STATUS.map((status) => ({
|
|
186
|
+
label: t(`status.${status}`),
|
|
187
|
+
value: status,
|
|
188
|
+
})),
|
|
199
189
|
]
|
|
200
|
-
}
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
// const tagItems = computed(() => {
|
|
193
|
+
// const allTags = new Set<string>()
|
|
194
|
+
// messageGroups.forEach((mg) => mg.tags?.forEach((tag) => allTags.add(tag)))
|
|
195
|
+
// return [...allTags].sort()
|
|
196
|
+
// })
|
|
197
|
+
|
|
198
|
+
// const selectedTags = ref<string[]>([])
|
|
201
199
|
|
|
202
|
-
const
|
|
203
|
-
toSelectItems(MESSAGE_GROUP_CATEGORIES, 'categories', t('common.all')),
|
|
204
|
-
)
|
|
205
|
-
const statusItems = computed(() => toSelectItems(MESSAGE_STATUS, 'status', t('common.all')))
|
|
206
|
-
const productItems = computed(() =>
|
|
207
|
-
toSelectItems(MESSAGE_GROUP_PRODUCTS, 'products', t('pages.message_library.all_products')),
|
|
208
|
-
)
|
|
209
|
-
|
|
210
|
-
const visibilityItems = computed(() =>
|
|
211
|
-
MESSAGE_GROUP_LIST_VISIBILITY.map((visibility) => ({
|
|
212
|
-
label: t(`pages.message_library.visibility.${visibility}`),
|
|
213
|
-
value: visibility,
|
|
214
|
-
})),
|
|
215
|
-
)
|
|
200
|
+
const table = useTemplateRef('table')
|
|
216
201
|
|
|
217
202
|
const messagesKind = computed(() => (props.renderType === 'email_v1' ? 'email' : 'whatsapp'))
|
|
218
203
|
|
|
204
|
+
watch(sorting, (val) => {
|
|
205
|
+
if (val[0]) {
|
|
206
|
+
trackEvent('table_sorting', {
|
|
207
|
+
messages_kind: messagesKind.value,
|
|
208
|
+
sort_column: val[0].id,
|
|
209
|
+
})
|
|
210
|
+
}
|
|
211
|
+
})
|
|
212
|
+
|
|
219
213
|
function trackFilters(overrides: Partial<TrackingEventMap['change_filters']> = {}) {
|
|
220
214
|
trackEvent('change_filters', {
|
|
221
|
-
search_term:
|
|
222
|
-
category:
|
|
223
|
-
status:
|
|
224
|
-
product: props.filters.product,
|
|
215
|
+
search_term: (table.value?.tableApi?.getColumn('name')?.getFilterValue() as string) || '',
|
|
216
|
+
category: (table.value?.tableApi?.getColumn('category')?.getFilterValue() as string) || '',
|
|
217
|
+
status: (table.value?.tableApi?.getColumn('status')?.getFilterValue() as string) || '',
|
|
225
218
|
messages_kind: messagesKind.value,
|
|
226
219
|
...overrides,
|
|
227
220
|
})
|
|
@@ -232,24 +225,19 @@ const trackSearchFilter = useDebounceFn((value: string) => {
|
|
|
232
225
|
}, 500)
|
|
233
226
|
|
|
234
227
|
function onSearchInput(value: string) {
|
|
235
|
-
|
|
228
|
+
table.value?.tableApi?.getColumn('name')?.setFilterValue(value)
|
|
236
229
|
trackSearchFilter(value)
|
|
237
230
|
}
|
|
238
231
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
if (key !== 'visibility') trackFilters({ [key]: value })
|
|
232
|
+
function onCategoryFilter(value: string) {
|
|
233
|
+
table.value?.tableApi?.getColumn('category')?.setFilterValue(value)
|
|
234
|
+
trackFilters({ category: value })
|
|
243
235
|
}
|
|
244
236
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
to: props.meta.to,
|
|
250
|
-
total: props.meta.total,
|
|
251
|
-
})
|
|
252
|
-
})
|
|
237
|
+
function onStatusFilter(value: string) {
|
|
238
|
+
table.value?.tableApi?.getColumn('status')?.setFilterValue(value)
|
|
239
|
+
trackFilters({ status: value })
|
|
240
|
+
}
|
|
253
241
|
</script>
|
|
254
242
|
|
|
255
243
|
<template>
|
|
@@ -259,46 +247,38 @@ const paginationSummary = computed(() => {
|
|
|
259
247
|
>
|
|
260
248
|
<div class="flex gap-2">
|
|
261
249
|
<UInput
|
|
262
|
-
:model-value="
|
|
250
|
+
:model-value="table?.tableApi?.getColumn('name')?.getFilterValue() as string"
|
|
263
251
|
class="max-w-xs"
|
|
264
252
|
leading-icon="ph:magnifying-glass-bold"
|
|
265
253
|
:placeholder="t('pages.message_library.search_placeholder')"
|
|
266
254
|
:data-testid="templateListTableTestIds.searchInput"
|
|
267
|
-
@update:model-value="onSearchInput(
|
|
255
|
+
@update:model-value="onSearchInput($event)"
|
|
268
256
|
/>
|
|
269
257
|
<USelect
|
|
270
|
-
:model-value="
|
|
258
|
+
:model-value="table?.tableApi?.getColumn('category')?.getFilterValue() as string"
|
|
271
259
|
class="max-w-48"
|
|
272
260
|
:placeholder="t('pages.message_library.category_placeholder')"
|
|
273
261
|
:items="categoryItems"
|
|
274
262
|
:data-testid="templateListTableTestIds.categorySelect"
|
|
275
|
-
@update:model-value="
|
|
263
|
+
@update:model-value="onCategoryFilter($event)"
|
|
276
264
|
/>
|
|
277
265
|
<USelect
|
|
278
|
-
:model-value="
|
|
266
|
+
:model-value="table?.tableApi?.getColumn('status')?.getFilterValue() as string"
|
|
279
267
|
class="max-w-48"
|
|
280
268
|
:placeholder="t('pages.message_library.status_placeholder')"
|
|
281
269
|
:items="statusItems"
|
|
282
270
|
:data-testid="templateListTableTestIds.statusSelect"
|
|
283
|
-
@update:model-value="
|
|
284
|
-
/>
|
|
285
|
-
<USelect
|
|
286
|
-
:model-value="filters.product"
|
|
287
|
-
class="max-w-48"
|
|
288
|
-
:placeholder="t('pages.message_library.product_placeholder')"
|
|
289
|
-
:items="productItems"
|
|
290
|
-
:data-testid="templateListTableTestIds.productSelect"
|
|
291
|
-
@update:model-value="onFilter('product', $event)"
|
|
292
|
-
/>
|
|
293
|
-
<USelect
|
|
294
|
-
v-if="appContext.isAdmin"
|
|
295
|
-
:model-value="filters.visibility"
|
|
296
|
-
class="max-w-48"
|
|
297
|
-
:placeholder="t('pages.message_library.visibility_placeholder')"
|
|
298
|
-
:items="visibilityItems"
|
|
299
|
-
:data-testid="templateListTableTestIds.visibilitySelect"
|
|
300
|
-
@update:model-value="onFilter('visibility', $event)"
|
|
271
|
+
@update:model-value="onStatusFilter($event)"
|
|
301
272
|
/>
|
|
273
|
+
<!-- <USelectMenu
|
|
274
|
+
v-if="tagItems.length > 0"
|
|
275
|
+
v-model="selectedTags"
|
|
276
|
+
:items="tagItems"
|
|
277
|
+
multiple
|
|
278
|
+
class="max-w-48"
|
|
279
|
+
:placeholder="t('pages.message_library.tags_placeholder')"
|
|
280
|
+
@update:model-value="table?.tableApi?.getColumn('tags')?.setFilterValue($event)"
|
|
281
|
+
/> -->
|
|
302
282
|
</div>
|
|
303
283
|
|
|
304
284
|
<UCard
|
|
@@ -315,10 +295,10 @@ const paginationSummary = computed(() => {
|
|
|
315
295
|
tr: 'hover:bg-muted',
|
|
316
296
|
root: 'overflow-visible static',
|
|
317
297
|
}"
|
|
318
|
-
:data="
|
|
298
|
+
:data="filteredMessageGroups"
|
|
319
299
|
:columns="columns"
|
|
320
|
-
:sorting-options="{ manualSorting: true, enableSortingRemoval: false }"
|
|
321
300
|
sticky
|
|
301
|
+
ref="table"
|
|
322
302
|
v-model:sorting="sorting"
|
|
323
303
|
:data-testid="templateListTableTestIds.table"
|
|
324
304
|
>
|
|
@@ -349,16 +329,6 @@ const paginationSummary = computed(() => {
|
|
|
349
329
|
/>
|
|
350
330
|
</template>
|
|
351
331
|
|
|
352
|
-
<template #product-cell="{ row }">
|
|
353
|
-
<UBadge
|
|
354
|
-
:label="t(`products.${row.original.product}`)"
|
|
355
|
-
size="md"
|
|
356
|
-
color="neutral"
|
|
357
|
-
variant="outline"
|
|
358
|
-
:data-testid="`${templateListTableTestIds.rowProductBadge}-${row.original.id}`"
|
|
359
|
-
/>
|
|
360
|
-
</template>
|
|
361
|
-
|
|
362
332
|
<template #status-cell="{ row }">
|
|
363
333
|
<StatusCell :items="row.original.messages" />
|
|
364
334
|
</template>
|
|
@@ -379,24 +349,6 @@ const paginationSummary = computed(() => {
|
|
|
379
349
|
/>
|
|
380
350
|
</template>
|
|
381
351
|
</UTable>
|
|
382
|
-
|
|
383
|
-
<template v-if="meta" #footer>
|
|
384
|
-
<div class="flex items-center justify-between gap-4">
|
|
385
|
-
<span
|
|
386
|
-
class="text-sm text-muted"
|
|
387
|
-
:data-testid="templateListTableTestIds.paginationSummary"
|
|
388
|
-
>
|
|
389
|
-
{{ paginationSummary }}
|
|
390
|
-
</span>
|
|
391
|
-
<UPagination
|
|
392
|
-
:page="page"
|
|
393
|
-
:total="meta.total"
|
|
394
|
-
:items-per-page="meta.per_page"
|
|
395
|
-
:data-testid="templateListTableTestIds.pagination"
|
|
396
|
-
@update:page="emit('update:page', $event)"
|
|
397
|
-
/>
|
|
398
|
-
</div>
|
|
399
|
-
</template>
|
|
400
352
|
</UCard>
|
|
401
353
|
</div>
|
|
402
354
|
</template>
|
|
@@ -112,10 +112,7 @@ function isMultimedia(model: HeaderBlockV1): model is WaHeaderTypeV1Url {
|
|
|
112
112
|
<div v-if="model.type === 'text'" class="mt-3">
|
|
113
113
|
<RichTextEditor
|
|
114
114
|
v-model:text="model.content"
|
|
115
|
-
preset="whatsapp"
|
|
116
|
-
bold
|
|
117
|
-
italic
|
|
118
|
-
emoji
|
|
115
|
+
preset="whatsapp-header"
|
|
119
116
|
improve
|
|
120
117
|
:character-limit="WA_HEADER_TEXT_MAX_LENGTH"
|
|
121
118
|
dynamic-values
|
|
@@ -9,6 +9,7 @@ import { computed } from 'vue'
|
|
|
9
9
|
import {
|
|
10
10
|
generateWhatsappHtml,
|
|
11
11
|
getTipTapExtensions,
|
|
12
|
+
sanitizeContent,
|
|
12
13
|
} from '@dev.smartpricing/message-composer-utils/utils'
|
|
13
14
|
import { useResolvedParametersQuery } from '~/queries/compilation'
|
|
14
15
|
|
|
@@ -51,9 +52,10 @@ const bodyHtml = computed(() => {
|
|
|
51
52
|
const headerHtml = computed(() => {
|
|
52
53
|
if (props.message.body.header.type !== 'text') return ''
|
|
53
54
|
if (!props.message.body.header.content) return ''
|
|
55
|
+
const extensions = getTipTapExtensions({ preset: 'whatsapp-header' })
|
|
54
56
|
const html = generateHTML(
|
|
55
|
-
props.message.body.header.content,
|
|
56
|
-
|
|
57
|
+
sanitizeContent(props.message.body.header.content, extensions),
|
|
58
|
+
extensions,
|
|
57
59
|
).replaceAll('<p></p>', '<p> </p>')
|
|
58
60
|
return replaceMentionsWithValues(html)
|
|
59
61
|
})
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { JSONContent } from '@tiptap/vue-3'
|
|
3
3
|
import { generateHTML } from '@tiptap/vue-3'
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
getTipTapExtensions,
|
|
6
|
+
sanitizeContent,
|
|
7
|
+
} from '@dev.smartpricing/message-composer-utils/utils'
|
|
5
8
|
import RichTextEditor from '@/components/Editor/RichTextEditor.vue'
|
|
6
9
|
import { useAppContextStore } from '@/stores/appContext'
|
|
7
10
|
import { WA_HEADER_TEXT_MAX_LENGTH } from '@dev.smartpricing/message-composer-utils/types'
|
|
@@ -23,7 +26,8 @@ const appContext = useAppContextStore()
|
|
|
23
26
|
// Generate HTML for preview of base language with rich text support
|
|
24
27
|
function generatePreviewHtml(content: JSONContent | undefined): string {
|
|
25
28
|
if (!content) return ''
|
|
26
|
-
|
|
29
|
+
const extensions = getTipTapExtensions({ preset: 'whatsapp-header' })
|
|
30
|
+
return generateHTML(sanitizeContent(content, extensions), extensions).replaceAll(
|
|
27
31
|
'<p></p>',
|
|
28
32
|
'<p> </p>',
|
|
29
33
|
)
|
|
@@ -45,10 +49,7 @@ function generatePreviewHtml(content: JSONContent | undefined): string {
|
|
|
45
49
|
<div class="flex-1">
|
|
46
50
|
<RichTextEditor
|
|
47
51
|
v-model:text="targetContent"
|
|
48
|
-
preset="whatsapp"
|
|
49
|
-
bold
|
|
50
|
-
italic
|
|
51
|
-
emoji
|
|
52
|
+
preset="whatsapp-header"
|
|
52
53
|
dynamic-values
|
|
53
54
|
:character-limit="WA_HEADER_TEXT_MAX_LENGTH"
|
|
54
55
|
:data-testid="whatsappTranslatorHeaderTestIds.targetEditor"
|
|
@@ -12,8 +12,7 @@ export interface ComposerContext {
|
|
|
12
12
|
/** @deprecated No longer used — auth is cookie-based via platform layer */
|
|
13
13
|
authenticationToken?: Ref<string>
|
|
14
14
|
organization_id?: Ref<string>
|
|
15
|
-
|
|
16
|
-
product: Ref<AppContextConfig['product']>
|
|
15
|
+
product?: Ref<AppContextConfig['product']>
|
|
17
16
|
strategy?: Ref<AppContextConfig['strategy']>
|
|
18
17
|
channels?: Ref<AppContextConfig['channels']>
|
|
19
18
|
languages?: Ref<string[]>
|
|
@@ -34,8 +33,7 @@ export function defineComposerContext(context: ComposerContext): void {
|
|
|
34
33
|
useNuxtApp().vueApp.provide(COMPOSER_CONTEXT_KEY, context)
|
|
35
34
|
}
|
|
36
35
|
|
|
37
|
-
|
|
38
|
-
const SERVER_FALLBACK = {} as ComposerContext
|
|
36
|
+
const SERVER_FALLBACK: ComposerContext = {}
|
|
39
37
|
|
|
40
38
|
export function useComposerContext(): ComposerContext {
|
|
41
39
|
if (import.meta.server) return SERVER_FALLBACK
|
|
@@ -13,7 +13,6 @@ import type {
|
|
|
13
13
|
MessageStatus,
|
|
14
14
|
MessageGroup,
|
|
15
15
|
} from '@dev.smartpricing/message-composer-utils/types'
|
|
16
|
-
import { useAppContextStore } from '@/stores/appContext'
|
|
17
16
|
|
|
18
17
|
interface TemplateData<T = Message | PostEntity<Message>> {
|
|
19
18
|
messageGroup: { name: string; category: MessageGroupCategory; brand_id?: string | null }
|
|
@@ -28,7 +27,6 @@ interface UseTemplateOperationsOptions {
|
|
|
28
27
|
export function useTemplateOperations(options: UseTemplateOperationsOptions = {}) {
|
|
29
28
|
const saving = ref(false)
|
|
30
29
|
const saved = ref(false)
|
|
31
|
-
const appContext = useAppContextStore()
|
|
32
30
|
|
|
33
31
|
// Hoist composables to setup scope (they use inject() internally)
|
|
34
32
|
const { mutateAsync: createMessageGroup } = useCreateMessageGroupMutation()
|
|
@@ -41,17 +39,9 @@ export function useTemplateOperations(options: UseTemplateOperationsOptions = {}
|
|
|
41
39
|
async function createTemplate(data: TemplateData, status: MessageStatus = 'draft') {
|
|
42
40
|
saving.value = true
|
|
43
41
|
try {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
throw new Error(
|
|
48
|
-
'[message-composer] Cannot create a template: no Product in the composer context',
|
|
49
|
-
)
|
|
50
|
-
}
|
|
51
|
-
const createdMessageGroup = await createMessageGroup({
|
|
52
|
-
...data.messageGroup,
|
|
53
|
-
product,
|
|
54
|
-
} as PostEntity<MessageGroup>)
|
|
42
|
+
const createdMessageGroup = await createMessageGroup(
|
|
43
|
+
data.messageGroup as PostEntity<MessageGroup>,
|
|
44
|
+
)
|
|
55
45
|
|
|
56
46
|
if (createdMessageGroup?.id) {
|
|
57
47
|
for (const message of Object.values(data.messages)) {
|
package/app/layouts/default.vue
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { useQuery } from '@pinia/colada'
|
|
2
|
-
import type { DynamicValuesResponse } from '@dev.smartpricing/message-composer-utils/types'
|
|
3
2
|
import { dynamicValuesApi } from '~/api'
|
|
4
3
|
import { useAppContextStore } from '~/stores/appContext'
|
|
5
4
|
|
|
@@ -7,13 +6,8 @@ export function useDynamicValuesQuery() {
|
|
|
7
6
|
const { product, mainLanguage } = storeToRefs(useAppContextStore())
|
|
8
7
|
|
|
9
8
|
return useQuery({
|
|
10
|
-
key: () => ['dynamic-values', product.value
|
|
11
|
-
|
|
12
|
-
// missing Product in `validationErrors`.
|
|
13
|
-
query: () => {
|
|
14
|
-
if (!product.value) return Promise.resolve<DynamicValuesResponse>({})
|
|
15
|
-
return dynamicValuesApi.get(product.value, mainLanguage.value || 'en')
|
|
16
|
-
},
|
|
9
|
+
key: () => ['dynamic-values', product.value, mainLanguage.value],
|
|
10
|
+
query: () => dynamicValuesApi.get(product.value || 'browser', mainLanguage.value || 'en'),
|
|
17
11
|
staleTime: 30 * 60 * 1000,
|
|
18
12
|
})
|
|
19
13
|
}
|
|
@@ -4,38 +4,36 @@ import type {
|
|
|
4
4
|
MessageGroup,
|
|
5
5
|
MessageGroupWithMessageSummary,
|
|
6
6
|
PostEntity,
|
|
7
|
+
RenderType,
|
|
7
8
|
} from '@dev.smartpricing/message-composer-utils/types'
|
|
8
9
|
import { messageGroupsApi } from '~/api'
|
|
9
|
-
import type { ListMessageGroupsOptions } from '~/api/messageGroups'
|
|
10
10
|
|
|
11
11
|
// QUERIES
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
queryOptions: { enabled?: MaybeRefOrGetter<boolean> } = {},
|
|
12
|
+
export function useMessageGroupsQuery(
|
|
13
|
+
options?: MaybeRefOrGetter<{
|
|
14
|
+
renderType?: RenderType
|
|
15
|
+
tags?: string[]
|
|
16
|
+
tagsMode?: 'and' | 'or'
|
|
17
|
+
}>,
|
|
18
|
+
_queryOptions?: { polling?: boolean },
|
|
20
19
|
) {
|
|
21
|
-
|
|
20
|
+
const query = useQuery({
|
|
22
21
|
key: () => ['message-groups', JSON.stringify(toValue(options))],
|
|
23
|
-
query: () => messageGroupsApi.
|
|
22
|
+
query: () => messageGroupsApi.getAll(toValue(options)),
|
|
24
23
|
staleTime: 0,
|
|
25
|
-
placeholderData: (previousData) => previousData,
|
|
26
|
-
enabled: queryOptions.enabled,
|
|
27
24
|
})
|
|
25
|
+
|
|
26
|
+
// TODO: Implement polling when Pinia Colada supports it
|
|
27
|
+
// For now, polling can be done manually in the component using setInterval + refetch
|
|
28
|
+
|
|
29
|
+
return query
|
|
28
30
|
}
|
|
29
31
|
|
|
30
|
-
export function useMessageGroupQuery(
|
|
31
|
-
id: MaybeRefOrGetter<string>,
|
|
32
|
-
queryOptions: { enabled?: MaybeRefOrGetter<boolean> } = {},
|
|
33
|
-
) {
|
|
32
|
+
export function useMessageGroupQuery(id: MaybeRefOrGetter<string>) {
|
|
34
33
|
return useQuery({
|
|
35
34
|
key: () => ['message-groups', toValue(id)],
|
|
36
35
|
query: () => messageGroupsApi.getById(toValue(id)),
|
|
37
36
|
staleTime: 2 * 60 * 1000,
|
|
38
|
-
enabled: queryOptions.enabled,
|
|
39
37
|
})
|
|
40
38
|
}
|
|
41
39
|
|
package/app/stores/appContext.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { ref, computed, watch } from 'vue'
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
type AppContextState,
|
|
2
|
+
import type {
|
|
3
|
+
AppContextConfig,
|
|
4
|
+
AppContextState,
|
|
6
5
|
} from '@dev.smartpricing/message-composer-utils/types'
|
|
7
6
|
|
|
8
7
|
export const useAppContextStore = defineStore('appContext', () => {
|
|
@@ -12,10 +11,9 @@ export const useAppContextStore = defineStore('appContext', () => {
|
|
|
12
11
|
|
|
13
12
|
// Proxy from injection with defaults (same interface as before)
|
|
14
13
|
const organization_id = computed(() => ctx.organization_id?.value ?? '')
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const product = computed<AppContextConfig['product'] | undefined>(() => ctx.product?.value)
|
|
14
|
+
const product = computed<NonNullable<AppContextConfig['product']>>(
|
|
15
|
+
() => ctx.product?.value ?? 'browser',
|
|
16
|
+
)
|
|
19
17
|
const strategy = computed<NonNullable<AppContextConfig['strategy']>>(
|
|
20
18
|
() => ctx.strategy?.value ?? 'native',
|
|
21
19
|
)
|
|
@@ -61,10 +59,6 @@ export const useAppContextStore = defineStore('appContext', () => {
|
|
|
61
59
|
const validationErrors = computed<Record<string, string>>(() => {
|
|
62
60
|
const errors: Record<string, string> = {}
|
|
63
61
|
|
|
64
|
-
if (!product.value) {
|
|
65
|
-
errors.product = `Product is required: pass \`product\` (${MESSAGE_GROUP_PRODUCTS.join(' | ')}) in the composer context`
|
|
66
|
-
}
|
|
67
|
-
|
|
68
62
|
if (channels.value?.includes('whatsapp') && !metaWabaId.value) {
|
|
69
63
|
errors.metaConfig = 'Meta WABA ID is required for WhatsApp'
|
|
70
64
|
}
|
package/app/types/tracking.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import type { MessageGroupProduct } from '@dev.smartpricing/message-composer-utils/types'
|
|
2
|
-
|
|
3
1
|
export type MessagesKind = 'email' | 'whatsapp'
|
|
4
2
|
|
|
5
3
|
export interface MessageActionProperties {
|
|
@@ -85,8 +83,6 @@ export interface TrackingEventMap {
|
|
|
85
83
|
search_term: string
|
|
86
84
|
category: string
|
|
87
85
|
status: string
|
|
88
|
-
/** The Product filter; `all` when not narrowed. */
|
|
89
|
-
product: MessageGroupProduct | 'all'
|
|
90
86
|
messages_kind: MessagesKind
|
|
91
87
|
}
|
|
92
88
|
change_tab: { messages_kind: MessagesKind }
|