@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
package/app/api/client.ts
CHANGED
|
@@ -15,7 +15,9 @@ function assertAppError(data: unknown): data is AppError {
|
|
|
15
15
|
export const apiClient = createApiClient({
|
|
16
16
|
baseURL: useRuntimeConfig().public.COMPOSER_API_URL as string,
|
|
17
17
|
csrf: {
|
|
18
|
-
|
|
18
|
+
// Same key and derivation as platform-layer's own client: the host sets the
|
|
19
|
+
// full cookie name (env-suffixed: smt_csrf / smt_csrf_stage / smt_csrf_dev).
|
|
20
|
+
cookieName: (useRuntimeConfig().public.CSRF_COOKIE_NAME as string) || 'smt_csrf',
|
|
19
21
|
},
|
|
20
22
|
auth: {
|
|
21
23
|
refreshBaseURL:
|
package/app/api/dynamicValues.ts
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
DynamicValuesResponse,
|
|
3
|
-
MessageGroupProduct,
|
|
4
|
-
} from '@dev.smartpricing/message-composer-utils/types'
|
|
1
|
+
import type { DynamicValuesResponse } from '@dev.smartpricing/message-composer-utils/types'
|
|
5
2
|
import { apiClient } from './client'
|
|
6
3
|
|
|
7
4
|
export const dynamicValuesApi = {
|
|
8
|
-
get: (product:
|
|
5
|
+
get: (product: string, language: string) => {
|
|
9
6
|
const params = new URLSearchParams()
|
|
10
7
|
params.set('product', product)
|
|
11
8
|
params.set('language', language)
|
package/app/api/messageGroups.ts
CHANGED
|
@@ -1,38 +1,27 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
MessageGroupFromDb,
|
|
3
|
-
PaginatedResponse,
|
|
4
3
|
MessageGroupWithMessages,
|
|
5
4
|
MessageGroupWithMessageSummary,
|
|
6
5
|
MessageGroupValidationResponse,
|
|
7
6
|
PostEntity,
|
|
8
7
|
MessageGroup,
|
|
8
|
+
RenderType,
|
|
9
9
|
DuplicateMessageGroupBody,
|
|
10
|
-
ListMessageGroupsQuery,
|
|
11
10
|
} from '@dev.smartpricing/message-composer-utils/types'
|
|
12
11
|
import { apiClient } from './client'
|
|
13
12
|
|
|
14
|
-
/**
|
|
15
|
-
* Query params of `GET /message-groups`, straight from the shared schema
|
|
16
|
-
* (ADR 0007): snake_case, as on the wire. `visibility` other than `public`
|
|
17
|
-
* is admin-only (403 otherwise); every field is optional here, the server
|
|
18
|
-
* applies the defaults.
|
|
19
|
-
*/
|
|
20
|
-
export type ListMessageGroupsOptions = Partial<ListMessageGroupsQuery>
|
|
21
|
-
|
|
22
|
-
function listQueryString(options: ListMessageGroupsOptions = {}): string {
|
|
23
|
-
const params = new URLSearchParams()
|
|
24
|
-
for (const [name, value] of Object.entries(options)) {
|
|
25
|
-
if (value !== undefined && value !== '') params.set(name, String(value))
|
|
26
|
-
}
|
|
27
|
-
return params.size ? `?${params.toString()}` : ''
|
|
28
|
-
}
|
|
29
|
-
|
|
30
13
|
export const messageGroupsApi = {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
14
|
+
getAll: (options?: { renderType?: RenderType; tags?: string[]; tagsMode?: 'and' | 'or' }) => {
|
|
15
|
+
const params = new URLSearchParams()
|
|
16
|
+
if (options?.renderType) params.set('render_type', options.renderType)
|
|
17
|
+
if (options?.tags && options.tags.length > 0) {
|
|
18
|
+
options.tags.forEach((tag) => params.append('tags', tag))
|
|
19
|
+
}
|
|
20
|
+
if (options?.tagsMode) params.set('tags_mode', options.tagsMode)
|
|
21
|
+
|
|
22
|
+
const query = params.size ? `?${params.toString()}` : ''
|
|
23
|
+
return apiClient<MessageGroupWithMessageSummary[]>(`/message-groups${query}`)
|
|
24
|
+
},
|
|
36
25
|
|
|
37
26
|
getById: (id: string) => apiClient<MessageGroupWithMessages>(`/message-groups/${id}`),
|
|
38
27
|
|
|
@@ -1,18 +1,9 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import type {
|
|
3
|
-
import {
|
|
4
|
-
import { useMessageGroupQuery } from '~/queries/messageGroups'
|
|
5
|
-
import { useAppContextStore } from '~/stores/appContext'
|
|
2
|
+
import type { RenderType, MessageStatus } from '@dev.smartpricing/message-composer-utils/types'
|
|
3
|
+
import { useMessageGroupsQuery } from '~/queries/messageGroups'
|
|
6
4
|
|
|
7
5
|
defineOptions({ name: 'MessageGroupSelect' })
|
|
8
6
|
|
|
9
|
-
/**
|
|
10
|
-
* In-composer Message Group picker (ADR 0012). Always lists the context
|
|
11
|
-
* Product's groups only: search is server-side, debounced and bound to the
|
|
12
|
-
* menu's search input; pages of 100 load on demand through "Load more".
|
|
13
|
-
* The selected group stays in the menu even when the loaded pages no longer
|
|
14
|
-
* contain it (search cleared, paging reset, or preselected beyond page one).
|
|
15
|
-
*/
|
|
16
7
|
const props = withDefaults(
|
|
17
8
|
defineProps<{
|
|
18
9
|
renderType?: RenderType
|
|
@@ -43,88 +34,11 @@ const emit = defineEmits<{
|
|
|
43
34
|
select: [item: { id: string; name: string }]
|
|
44
35
|
}>()
|
|
45
36
|
|
|
46
|
-
/** What the picker needs from a group, whichever endpoint it came from. */
|
|
47
|
-
interface PickerGroup {
|
|
48
|
-
id: string
|
|
49
|
-
name: string
|
|
50
|
-
messages: Array<{ status: MessageStatus }>
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const PER_PAGE = 100
|
|
54
|
-
|
|
55
37
|
const { t } = useI18n()
|
|
56
|
-
const appContext = useAppContextStore()
|
|
57
38
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const searchTerm = ref('')
|
|
62
|
-
const page = ref(1)
|
|
63
|
-
|
|
64
|
-
// Without a context Product the server would answer with every Product's
|
|
65
|
-
// groups (ADR 0011): never ask. The host's missing Product is already reported
|
|
66
|
-
// by the app-context validation.
|
|
67
|
-
const {
|
|
68
|
-
query: { data: envelope, isPlaceholderData },
|
|
69
|
-
loading,
|
|
70
|
-
} = usePagedMessageGroups({
|
|
71
|
-
search: searchTerm,
|
|
72
|
-
page,
|
|
73
|
-
perPage: PER_PAGE,
|
|
74
|
-
renderType: () => props.renderType,
|
|
75
|
-
filters: () => ({
|
|
76
|
-
product: appContext.product,
|
|
77
|
-
visibility: appContext.isAdmin ? 'all' : 'public',
|
|
78
|
-
}),
|
|
79
|
-
resetOn: [() => appContext.product],
|
|
80
|
-
enabled: () => appContext.product != null,
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
// Pages loaded so far, indexed by page number: page one replaces everything,
|
|
84
|
-
// later pages slot in at their index (a refetch replaces, never duplicates).
|
|
85
|
-
const pages = shallowRef<PickerGroup[][]>([])
|
|
86
|
-
watch(
|
|
87
|
-
envelope,
|
|
88
|
-
(value) => {
|
|
89
|
-
if (!value) return
|
|
90
|
-
const index = value.meta.current_page - 1
|
|
91
|
-
pages.value = index === 0 ? [value.data] : [...pages.value.slice(0, index), value.data]
|
|
92
|
-
},
|
|
93
|
-
{ immediate: true },
|
|
94
|
-
)
|
|
95
|
-
const loadedGroups = computed(() => pages.value.flat())
|
|
96
|
-
|
|
97
|
-
// The selected group is pinned on top of the loaded pages when they do not
|
|
98
|
-
// contain it: remembered from the last pick, or fetched by id when preselected.
|
|
99
|
-
const lastPicked = shallowRef<PickerGroup>()
|
|
100
|
-
const missingSelectedId = computed(() => {
|
|
101
|
-
const id = modelValue.value
|
|
102
|
-
if (!id || loadedGroups.value.some((g) => g.id === id)) return undefined
|
|
103
|
-
return lastPicked.value?.id === id ? undefined : id
|
|
104
|
-
})
|
|
105
|
-
const { data: fetchedSelected } = useMessageGroupQuery(() => missingSelectedId.value ?? '', {
|
|
106
|
-
enabled: () => missingSelectedId.value != null,
|
|
107
|
-
})
|
|
108
|
-
const messageGroups = computed<PickerGroup[]>(() => {
|
|
109
|
-
const id = modelValue.value
|
|
110
|
-
if (!id || loadedGroups.value.some((g) => g.id === id)) return loadedGroups.value
|
|
111
|
-
const pinned =
|
|
112
|
-
lastPicked.value?.id === id
|
|
113
|
-
? lastPicked.value
|
|
114
|
-
: fetchedSelected.value?.id === id
|
|
115
|
-
? fetchedSelected.value
|
|
116
|
-
: undefined
|
|
117
|
-
return pinned ? [pinned, ...loadedGroups.value] : loadedGroups.value
|
|
118
|
-
})
|
|
119
|
-
|
|
120
|
-
const hasMore = computed(
|
|
121
|
-
() =>
|
|
122
|
-
!isPlaceholderData.value &&
|
|
123
|
-
envelope.value != null &&
|
|
124
|
-
envelope.value.meta.current_page < envelope.value.meta.last_page,
|
|
125
|
-
)
|
|
126
|
-
const isLoadingFirstPage = computed(() => page.value === 1 && loading.value)
|
|
127
|
-
const isLoadingMore = computed(() => page.value > 1 && loading.value)
|
|
39
|
+
const { data: messageGroups, isLoading } = useMessageGroupsQuery(() => ({
|
|
40
|
+
renderType: props.renderType,
|
|
41
|
+
}))
|
|
128
42
|
|
|
129
43
|
const hideInactive = ref(false)
|
|
130
44
|
|
|
@@ -133,6 +47,7 @@ function isGroupActive(messages: Array<{ status: MessageStatus }>): boolean {
|
|
|
133
47
|
}
|
|
134
48
|
|
|
135
49
|
const items = computed(() => {
|
|
50
|
+
if (!messageGroups.value) return []
|
|
136
51
|
return messageGroups.value.reduce<
|
|
137
52
|
Array<{
|
|
138
53
|
label: string
|
|
@@ -165,21 +80,18 @@ const items = computed(() => {
|
|
|
165
80
|
|
|
166
81
|
function handleUpdate(value: string) {
|
|
167
82
|
modelValue.value = value
|
|
168
|
-
const group = messageGroups.value
|
|
169
|
-
lastPicked.value = group
|
|
83
|
+
const group = messageGroups.value?.find((g) => g.id === value)
|
|
170
84
|
emit('select', { id: value, name: group?.name ?? '' })
|
|
171
85
|
}
|
|
172
86
|
</script>
|
|
173
87
|
|
|
174
88
|
<template>
|
|
175
89
|
<USelectMenu
|
|
176
|
-
v-model:search-term="searchTerm"
|
|
177
90
|
:model-value="modelValue"
|
|
178
91
|
:items="items"
|
|
179
92
|
value-key="value"
|
|
180
|
-
:loading="
|
|
93
|
+
:loading="isLoading"
|
|
181
94
|
searchable
|
|
182
|
-
ignore-filter
|
|
183
95
|
:placeholder="placeholder"
|
|
184
96
|
:search-input="{ placeholder: searchPlaceholder }"
|
|
185
97
|
:data-testid="messageGroupSelectTestIds.root"
|
|
@@ -200,18 +112,5 @@ function handleUpdate(value: string) {
|
|
|
200
112
|
</span>
|
|
201
113
|
</div>
|
|
202
114
|
</template>
|
|
203
|
-
|
|
204
|
-
<template v-if="hasMore || isLoadingMore" #content-bottom>
|
|
205
|
-
<div class="flex justify-center px-2 py-1.5 border-t border-default">
|
|
206
|
-
<UButton
|
|
207
|
-
variant="link"
|
|
208
|
-
size="xs"
|
|
209
|
-
:loading="isLoadingMore"
|
|
210
|
-
:label="t('common.messageGroupSelect.load_more')"
|
|
211
|
-
:data-testid="messageGroupSelectTestIds.loadMore"
|
|
212
|
-
@click="page++"
|
|
213
|
-
/>
|
|
214
|
-
</div>
|
|
215
|
-
</template>
|
|
216
115
|
</USelectMenu>
|
|
217
116
|
</template>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import {
|
|
3
|
+
useMessageGroupsQuery,
|
|
3
4
|
useDeleteMessageGroupMutation,
|
|
4
5
|
usePublishMessageGroupMutation,
|
|
5
6
|
useUnpublishMessageGroupMutation,
|
|
@@ -9,6 +10,7 @@ import type {
|
|
|
9
10
|
RenderType,
|
|
10
11
|
} from '@dev.smartpricing/message-composer-utils/types'
|
|
11
12
|
import type { MessageActionProperties, MessagesKind } from '@/types/tracking'
|
|
13
|
+
import { computed } from 'vue'
|
|
12
14
|
import BaseTable from '@/components/TemplateList/BaseTable.vue'
|
|
13
15
|
|
|
14
16
|
const renderType = defineModel<RenderType>('renderType', { required: true })
|
|
@@ -24,17 +26,12 @@ const emit = defineEmits<{
|
|
|
24
26
|
const { trackEvent } = useTracking()
|
|
25
27
|
const { t } = useI18n()
|
|
26
28
|
|
|
27
|
-
//
|
|
29
|
+
// Query
|
|
28
30
|
const {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
setSorting,
|
|
32
|
-
setPage,
|
|
33
|
-
rows: messageGroups,
|
|
34
|
-
meta,
|
|
35
|
-
loading: isFetching,
|
|
31
|
+
data: messageGroups,
|
|
32
|
+
isPending: isFetching,
|
|
36
33
|
refetch: execute,
|
|
37
|
-
} =
|
|
34
|
+
} = useMessageGroupsQuery(computed(() => ({ renderType: renderType.value })))
|
|
38
35
|
|
|
39
36
|
// Polling
|
|
40
37
|
const interval = setInterval(() => execute(), 15000)
|
|
@@ -147,15 +144,8 @@ defineExpose({ execute })
|
|
|
147
144
|
<template>
|
|
148
145
|
<BaseTable
|
|
149
146
|
:loading="isFetching"
|
|
150
|
-
:message-groups="messageGroups"
|
|
151
|
-
:meta="meta"
|
|
147
|
+
:message-groups="messageGroups || []"
|
|
152
148
|
:render-type="renderType"
|
|
153
|
-
:filters="listState"
|
|
154
|
-
:sorting="listState"
|
|
155
|
-
:page="listState.page"
|
|
156
|
-
@update:filters="setFilters"
|
|
157
|
-
@update:sorting="setSorting"
|
|
158
|
-
@update:page="setPage"
|
|
159
149
|
@edit="navigate"
|
|
160
150
|
@delete="askDeleteGroup"
|
|
161
151
|
@duplicate="handleDuplicate"
|
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
} from '@dev.smartpricing/message-composer-utils/types'
|
|
11
11
|
import {
|
|
12
12
|
buildLinkHTMLAttributes,
|
|
13
|
+
convertToWhatsappHeaderText,
|
|
13
14
|
convertToWhatsappMarkdown,
|
|
14
15
|
getTipTapExtensions,
|
|
15
16
|
sanitizeContent,
|
|
@@ -25,7 +26,7 @@ const { t } = useI18n()
|
|
|
25
26
|
|
|
26
27
|
const props = withDefaults(
|
|
27
28
|
defineProps<{
|
|
28
|
-
preset: 'whatsapp' | 'email'
|
|
29
|
+
preset: 'whatsapp' | 'whatsapp-header' | 'email'
|
|
29
30
|
characterLimit?: number
|
|
30
31
|
dynamicValues?: boolean
|
|
31
32
|
defaultContext?: string
|
|
@@ -277,9 +278,12 @@ const toolbarItems = computed(() => {
|
|
|
277
278
|
// WhatsApp markdown-aware character count
|
|
278
279
|
// Meta counts the final markdown output (with *bold*, _italic_ marks),
|
|
279
280
|
// so we must display that count instead of TipTap's document-model count.
|
|
281
|
+
const isWhatsapp = computed(() => props.preset === 'whatsapp' || props.preset === 'whatsapp-header')
|
|
280
282
|
const whatsappCharCount = computed(() => {
|
|
281
|
-
if (
|
|
282
|
-
return
|
|
283
|
+
if (!isWhatsapp.value || !props.characterLimit || !text.value) return 0
|
|
284
|
+
return props.preset === 'whatsapp-header'
|
|
285
|
+
? convertToWhatsappHeaderText(text.value).length
|
|
286
|
+
: convertToWhatsappMarkdown(text.value).length
|
|
283
287
|
})
|
|
284
288
|
|
|
285
289
|
// Handlers
|
|
@@ -482,7 +486,7 @@ defineExpose({
|
|
|
482
486
|
{{ $t('editor.validation.character_limit') }}
|
|
483
487
|
</span>
|
|
484
488
|
<span>
|
|
485
|
-
{{
|
|
489
|
+
{{ isWhatsapp ? whatsappCharCount : editor.storage.characterCount.characters() }}
|
|
486
490
|
/ {{ characterLimit }}
|
|
487
491
|
</span>
|
|
488
492
|
</p>
|