@dev.smartpricing/message-composer-layer 4.5.1-next.0 → 4.5.1-next.2
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/README.md +1 -1
- package/app/components/Editor/RichTextEditor.vue +14 -2
- package/app/components/TemplateList/BaseTable.vue +11 -0
- package/app/components/Whatsapp/Preview.vue +5 -12
- package/app/components/Whatsapp/Translator/Header.vue +4 -19
- package/app/composables/composerContext.ts +6 -0
- package/app/composables/useMessageGroupWaba.ts +7 -4
- package/app/queries/messageGroups.ts +3 -1
- package/app/stores/appContext.ts +3 -0
- package/app/utils/testIds.const.ts +1 -0
- package/i18n/locales/de.ts +3 -0
- package/i18n/locales/en.ts +3 -0
- package/i18n/locales/es.ts +3 -0
- package/i18n/locales/fr.ts +3 -0
- package/i18n/locales/it.ts +3 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -85,7 +85,7 @@ watch(
|
|
|
85
85
|
)
|
|
86
86
|
```
|
|
87
87
|
|
|
88
|
-
**WABA.** The Host does not supply the WABA: the WABAs follow the Organization and the backend resolves them (ADR [0015](../../docs/adr/0015-message-group-waba-fixed-at-first-submission.md)). WhatsApp is configured when the Host's `channels` include `whatsapp` and the Organization has at least one WABA. A Message Group is submitted to one WABA, chosen at its first submission and fixed from then on; with several WABAs the user picks one when requesting approval. `metaWabaId` in the context is optional and is only the preselection in that picker.
|
|
88
|
+
**WABA.** The Host does not supply the WABA: the WABAs follow the Organization and the backend resolves them (ADR [0015](../../docs/adr/0015-message-group-waba-fixed-at-first-submission.md)). WhatsApp is configured when the Host's `channels` include `whatsapp` and the Organization has at least one WABA. A Message Group is submitted to one WABA, chosen at its first submission and fixed from then on; with several WABAs the user picks one when requesting approval. `metaWabaId` in the context is optional and is only the preselection in that picker. A Host that sends from one WABA only also sets `metaWabaLocked: ref(true)`: a new Message Group is then submitted to `metaWabaId` without asking, as long as it is one of the Organization's WABAs.
|
|
89
89
|
|
|
90
90
|
### Upgrading to 4.5.0
|
|
91
91
|
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
convertToWhatsappHeaderText,
|
|
14
14
|
convertToWhatsappMarkdown,
|
|
15
15
|
getTipTapExtensions,
|
|
16
|
+
isWhatsappHeaderMultiline,
|
|
16
17
|
sanitizeContent,
|
|
17
18
|
} from '@dev.smartpricing/message-composer-utils/utils'
|
|
18
19
|
import EmojiPickerButton from '../Common/EmojiPickerButton.vue'
|
|
@@ -278,14 +279,21 @@ const toolbarItems = computed(() => {
|
|
|
278
279
|
// WhatsApp markdown-aware character count
|
|
279
280
|
// Meta counts the final markdown output (with *bold*, _italic_ marks),
|
|
280
281
|
// so we must display that count instead of TipTap's document-model count.
|
|
281
|
-
const
|
|
282
|
+
const isWhatsappHeader = computed(() => props.preset === 'whatsapp-header')
|
|
283
|
+
const isWhatsapp = computed(() => props.preset === 'whatsapp' || isWhatsappHeader.value)
|
|
282
284
|
const whatsappCharCount = computed(() => {
|
|
283
285
|
if (!isWhatsapp.value || !props.characterLimit || !text.value) return 0
|
|
284
|
-
return
|
|
286
|
+
return isWhatsappHeader.value
|
|
285
287
|
? convertToWhatsappHeaderText(text.value).length
|
|
286
288
|
: convertToWhatsappMarkdown(text.value).length
|
|
287
289
|
})
|
|
288
290
|
|
|
291
|
+
// A multi-line header is still saved (its lines are joined into one when
|
|
292
|
+
// sent to Meta), so the editor warns instead of raising a validation error.
|
|
293
|
+
const hasHeaderLineBreaks = computed(
|
|
294
|
+
() => isWhatsappHeader.value && isWhatsappHeaderMultiline(text.value),
|
|
295
|
+
)
|
|
296
|
+
|
|
289
297
|
// Handlers
|
|
290
298
|
function onSelectEmoji(emoji: EmojiExt) {
|
|
291
299
|
editor.value?.commands.insertContent(emoji.i)
|
|
@@ -491,6 +499,10 @@ defineExpose({
|
|
|
491
499
|
</span>
|
|
492
500
|
</p>
|
|
493
501
|
|
|
502
|
+
<p v-if="hasHeaderLineBreaks" class="paragraph-xs text-warning-700 pt-1.5 px-1">
|
|
503
|
+
{{ $t('editor.validation.header_line_breaks') }}
|
|
504
|
+
</p>
|
|
505
|
+
|
|
494
506
|
<!-- AI Generate slideover -->
|
|
495
507
|
</div>
|
|
496
508
|
</template>
|
|
@@ -322,6 +322,17 @@ const paginationSummary = computed(() => {
|
|
|
322
322
|
v-model:sorting="sorting"
|
|
323
323
|
:data-testid="templateListTableTestIds.table"
|
|
324
324
|
>
|
|
325
|
+
<!-- Shown instead of the "no data" row while a list with no rows is loading. -->
|
|
326
|
+
<template #loading>
|
|
327
|
+
<div
|
|
328
|
+
class="flex items-center justify-center gap-2 text-muted"
|
|
329
|
+
:data-testid="templateListTableTestIds.loading"
|
|
330
|
+
>
|
|
331
|
+
<UIcon name="ph:circle-notch-bold" class="size-4 animate-spin" />
|
|
332
|
+
<span>{{ t('pages.message_library.loading') }}</span>
|
|
333
|
+
</div>
|
|
334
|
+
</template>
|
|
335
|
+
|
|
325
336
|
<template #name-cell="{ row }">
|
|
326
337
|
<div
|
|
327
338
|
class="flex items-center gap-2"
|
|
@@ -4,12 +4,10 @@ import type {
|
|
|
4
4
|
WaButtonTypeV1,
|
|
5
5
|
WhatsappMessage,
|
|
6
6
|
} from '@dev.smartpricing/message-composer-utils/types'
|
|
7
|
-
import { generateHTML } from '@tiptap/vue-3'
|
|
8
7
|
import { computed } from 'vue'
|
|
9
8
|
import {
|
|
9
|
+
convertToWhatsappHeaderText,
|
|
10
10
|
generateWhatsappHtml,
|
|
11
|
-
getTipTapExtensions,
|
|
12
|
-
sanitizeContent,
|
|
13
11
|
} from '@dev.smartpricing/message-composer-utils/utils'
|
|
14
12
|
import { useResolvedParametersQuery } from '~/queries/compilation'
|
|
15
13
|
|
|
@@ -57,15 +55,10 @@ const bodyHtml = computed(() => {
|
|
|
57
55
|
return replaceMentionsWithValues(html)
|
|
58
56
|
})
|
|
59
57
|
|
|
60
|
-
|
|
58
|
+
// Plain text, exactly as Meta receives it
|
|
59
|
+
const headerText = computed(() => {
|
|
61
60
|
if (props.message.body.header.type !== 'text') return ''
|
|
62
|
-
|
|
63
|
-
const extensions = getTipTapExtensions({ preset: 'whatsapp-header' })
|
|
64
|
-
const html = generateHTML(
|
|
65
|
-
sanitizeContent(props.message.body.header.content, extensions),
|
|
66
|
-
extensions,
|
|
67
|
-
).replaceAll('<p></p>', '<p> </p>')
|
|
68
|
-
return replaceMentionsWithValues(html)
|
|
61
|
+
return replaceMentionsWithValues(convertToWhatsappHeaderText(props.message.body.header.content))
|
|
69
62
|
})
|
|
70
63
|
|
|
71
64
|
const currentTime = computed(() => {
|
|
@@ -131,7 +124,7 @@ const getIcon = (type: WaButtonTypeV1) => {
|
|
|
131
124
|
v-if="message.body.header.type === 'text'"
|
|
132
125
|
class="text-sm text-primary-950 break-all"
|
|
133
126
|
>
|
|
134
|
-
<p
|
|
127
|
+
<p>{{ headerText }}</p>
|
|
135
128
|
</div>
|
|
136
129
|
<div v-else-if="message.body.header.type === 'image'" class="mb-2">
|
|
137
130
|
<img
|
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { JSONContent } from '@tiptap/vue-3'
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
getTipTapExtensions,
|
|
6
|
-
sanitizeContent,
|
|
7
|
-
} from '@dev.smartpricing/message-composer-utils/utils'
|
|
3
|
+
import { convertToWhatsappHeaderText } from '@dev.smartpricing/message-composer-utils/utils'
|
|
8
4
|
import RichTextEditor from '@/components/Editor/RichTextEditor.vue'
|
|
9
5
|
import { useAppContextStore } from '@/stores/appContext'
|
|
10
6
|
import { WA_HEADER_TEXT_MAX_LENGTH } from '@dev.smartpricing/message-composer-utils/types'
|
|
@@ -22,26 +18,15 @@ const targetContent = defineModel<JSONContent | undefined>({
|
|
|
22
18
|
})
|
|
23
19
|
|
|
24
20
|
const appContext = useAppContextStore()
|
|
25
|
-
|
|
26
|
-
// Generate HTML for preview of base language with rich text support
|
|
27
|
-
function generatePreviewHtml(content: JSONContent | undefined): string {
|
|
28
|
-
if (!content) return ''
|
|
29
|
-
const extensions = getTipTapExtensions({ preset: 'whatsapp-header' })
|
|
30
|
-
return generateHTML(sanitizeContent(content, extensions), extensions).replaceAll(
|
|
31
|
-
'<p></p>',
|
|
32
|
-
'<p> </p>',
|
|
33
|
-
)
|
|
34
|
-
}
|
|
35
21
|
</script>
|
|
36
22
|
|
|
37
23
|
<template>
|
|
38
24
|
<div class="flex gap-8 items-start" :data-testid="whatsappTranslatorHeaderTestIds.container">
|
|
39
25
|
<!-- Base language preview (read-only) -->
|
|
40
26
|
<div class="flex-1 min-w-0" :data-testid="whatsappTranslatorHeaderTestIds.basePreview">
|
|
41
|
-
<div
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
/>
|
|
27
|
+
<div class="paragraph-sm text-primary-700 break-words">
|
|
28
|
+
{{ convertToWhatsappHeaderText(baseContent) }}
|
|
29
|
+
</div>
|
|
45
30
|
</div>
|
|
46
31
|
|
|
47
32
|
<!-- Translation editor (editable) -->
|
|
@@ -22,6 +22,12 @@ export interface ComposerContext {
|
|
|
22
22
|
* the Organization's (ADR 0015).
|
|
23
23
|
*/
|
|
24
24
|
metaWabaId?: Ref<string>
|
|
25
|
+
/**
|
|
26
|
+
* The Host works on `metaWabaId` only (Smartconnect): a new Message Group
|
|
27
|
+
* is submitted to it and the user is not asked. Applies while `metaWabaId`
|
|
28
|
+
* is one of the Organization's WABAs; otherwise the choice stays the user's.
|
|
29
|
+
*/
|
|
30
|
+
metaWabaLocked?: Ref<boolean>
|
|
25
31
|
isAdmin?: Ref<boolean>
|
|
26
32
|
organizationData?: Ref<OrganizationData>
|
|
27
33
|
}
|
|
@@ -9,7 +9,7 @@ import WabaSelectModal from '~/components/Composer/WabaSelectModal.vue'
|
|
|
9
9
|
*/
|
|
10
10
|
export function useMessageGroupWaba(storedWabaId: MaybeRefOrGetter<string | null | undefined>) {
|
|
11
11
|
const { data: wabas, refresh } = useOrganizationWabasQuery()
|
|
12
|
-
const { metaWabaId } = storeToRefs(useAppContextStore())
|
|
12
|
+
const { metaWabaId, metaWabaLocked } = storeToRefs(useAppContextStore())
|
|
13
13
|
const modal = useOverlay().create(WabaSelectModal)
|
|
14
14
|
|
|
15
15
|
// A submission of one language does not read the Message Group again: its
|
|
@@ -45,9 +45,10 @@ export function useMessageGroupWaba(storedWabaId: MaybeRefOrGetter<string | null
|
|
|
45
45
|
/**
|
|
46
46
|
* The WABA to name in a submission. Asks the user only at the first
|
|
47
47
|
* submission of a Message Group whose Organization has several WABAs;
|
|
48
|
-
* otherwise names none and the backend resolves it. A
|
|
49
|
-
*
|
|
50
|
-
*
|
|
48
|
+
* otherwise names none and the backend resolves it. A Host that works on
|
|
49
|
+
* its own WABA only (`metaWabaLocked`) is not asked: that WABA is named.
|
|
50
|
+
* A lookup that has not answered yet is waited for; one that failed leaves
|
|
51
|
+
* it to the backend. Returns null when the user gives up.
|
|
51
52
|
*/
|
|
52
53
|
async function chooseWaba(): Promise<{ wabaId?: string } | null> {
|
|
53
54
|
if (wabaId.value) return {}
|
|
@@ -58,6 +59,8 @@ export function useMessageGroupWaba(storedWabaId: MaybeRefOrGetter<string | null
|
|
|
58
59
|
const preselected = candidates.some((candidate) => candidate.id === metaWabaId.value)
|
|
59
60
|
? metaWabaId.value
|
|
60
61
|
: undefined
|
|
62
|
+
if (preselected && metaWabaLocked.value) return { wabaId: preselected }
|
|
63
|
+
|
|
61
64
|
const chosen = await modal.open({ wabas: candidates, preselected }).result
|
|
62
65
|
|
|
63
66
|
return chosen ? { wabaId: chosen } : null
|
|
@@ -56,7 +56,9 @@ export function useMessageGroupListQuery(
|
|
|
56
56
|
// Never across Organizations: a switch must not show the previous one's rows.
|
|
57
57
|
placeholderData: (previousData, previousEntry) =>
|
|
58
58
|
previousEntry?.key.at(-1) === listOrganizationId() ? previousData : undefined,
|
|
59
|
-
|
|
59
|
+
// Until an Organization is resolved the list waits (pending, so callers
|
|
60
|
+
// show it as loading) instead of failing into an empty table.
|
|
61
|
+
enabled: () => toValue(queryOptions.enabled ?? true) && !!listOrganizationId(),
|
|
60
62
|
})
|
|
61
63
|
}
|
|
62
64
|
|
package/app/stores/appContext.ts
CHANGED
|
@@ -25,6 +25,8 @@ export const useAppContextStore = defineStore('appContext', () => {
|
|
|
25
25
|
const isAdmin = computed(() => ctx.isAdmin?.value ?? false)
|
|
26
26
|
// The Host's preselection among the Organization's WABAs (ADR 0015).
|
|
27
27
|
const metaWabaId = computed(() => ctx.metaWabaId?.value || undefined)
|
|
28
|
+
// The Host's WABA is the only one it works on: no choice is offered.
|
|
29
|
+
const metaWabaLocked = computed(() => ctx.metaWabaLocked?.value ?? false)
|
|
28
30
|
|
|
29
31
|
// Sync i18n locale when mainLanguage changes
|
|
30
32
|
watch(
|
|
@@ -58,6 +60,7 @@ export const useAppContextStore = defineStore('appContext', () => {
|
|
|
58
60
|
default_tags,
|
|
59
61
|
isAdmin,
|
|
60
62
|
metaWabaId,
|
|
63
|
+
metaWabaLocked,
|
|
61
64
|
|
|
62
65
|
validationErrors,
|
|
63
66
|
}
|
|
@@ -519,6 +519,7 @@ export const templateListTableTestIds = {
|
|
|
519
519
|
paginationSummary: 'template-list-table-pagination-summary',
|
|
520
520
|
card: 'template-list-table-card',
|
|
521
521
|
table: 'template-list-table-table',
|
|
522
|
+
loading: 'template-list-table-loading',
|
|
522
523
|
rowName: 'template-list-table-row-name',
|
|
523
524
|
rowInternalBadge: 'template-list-table-row-internal-badge',
|
|
524
525
|
rowCategoryBadge: 'template-list-table-row-category-badge',
|
package/i18n/locales/de.ts
CHANGED
|
@@ -512,6 +512,8 @@ export default {
|
|
|
512
512
|
},
|
|
513
513
|
validation: {
|
|
514
514
|
character_limit: 'Dieses Feld hat eine Zeichenbegrenzung.',
|
|
515
|
+
header_line_breaks:
|
|
516
|
+
'Zeilenumbrüche werden in der Kopfzeile nicht unterstützt: Die Zeilen werden zu einer zusammengefügt.',
|
|
515
517
|
error: {
|
|
516
518
|
atLeastOneBlockRequired: 'Mindestens ein Inhaltsblock ist erforderlich',
|
|
517
519
|
bodyEmpty: 'Nachrichtentext darf nicht leer sein',
|
|
@@ -823,6 +825,7 @@ export default {
|
|
|
823
825
|
status: 'Status',
|
|
824
826
|
updated_at: 'Aktualisiert am',
|
|
825
827
|
},
|
|
828
|
+
loading: 'Nachrichten werden geladen...',
|
|
826
829
|
pagination_summary: '{from}–{to} von {total}',
|
|
827
830
|
product_placeholder: 'Produkt auswählen',
|
|
828
831
|
render_type_placeholder: 'Render-Typ auswählen',
|
package/i18n/locales/en.ts
CHANGED
|
@@ -506,6 +506,8 @@ export default {
|
|
|
506
506
|
},
|
|
507
507
|
validation: {
|
|
508
508
|
character_limit: 'This field has a character limit.',
|
|
509
|
+
header_line_breaks:
|
|
510
|
+
"Line breaks aren't supported in the header: the lines will be joined into one.",
|
|
509
511
|
error: {
|
|
510
512
|
atLeastOneBlockRequired: 'At least one content block is required',
|
|
511
513
|
bodyEmpty: 'Message body cannot be empty',
|
|
@@ -812,6 +814,7 @@ export default {
|
|
|
812
814
|
status: 'Status',
|
|
813
815
|
updated_at: 'Updated at',
|
|
814
816
|
},
|
|
817
|
+
loading: 'Loading messages...',
|
|
815
818
|
pagination_summary: '{from}–{to} of {total}',
|
|
816
819
|
product_placeholder: 'Select product',
|
|
817
820
|
render_type_placeholder: 'Select render type',
|
package/i18n/locales/es.ts
CHANGED
|
@@ -508,6 +508,8 @@ export default {
|
|
|
508
508
|
},
|
|
509
509
|
validation: {
|
|
510
510
|
character_limit: 'Este campo tiene un límite de caracteres.',
|
|
511
|
+
header_line_breaks:
|
|
512
|
+
'Los saltos de línea no se admiten en el encabezado: las líneas se unirán en una sola.',
|
|
511
513
|
error: {
|
|
512
514
|
atLeastOneBlockRequired: 'Se requiere al menos un bloque de contenido',
|
|
513
515
|
bodyEmpty: 'El cuerpo del mensaje no puede estar vacío',
|
|
@@ -821,6 +823,7 @@ export default {
|
|
|
821
823
|
status: 'Estado',
|
|
822
824
|
updated_at: 'Actualizado el',
|
|
823
825
|
},
|
|
826
|
+
loading: 'Cargando mensajes...',
|
|
824
827
|
pagination_summary: '{from}–{to} de {total}',
|
|
825
828
|
product_placeholder: 'Seleccionar producto',
|
|
826
829
|
render_type_placeholder: 'Seleccionar tipo de renderizado',
|
package/i18n/locales/fr.ts
CHANGED
|
@@ -509,6 +509,8 @@ export default {
|
|
|
509
509
|
},
|
|
510
510
|
validation: {
|
|
511
511
|
character_limit: 'Ce champ a une limite de caractères.',
|
|
512
|
+
header_line_breaks:
|
|
513
|
+
"Les sauts de ligne ne sont pas pris en charge dans l'en-tête : les lignes seront fusionnées en une seule.",
|
|
512
514
|
error: {
|
|
513
515
|
atLeastOneBlockRequired: 'Au moins un bloc de contenu est requis',
|
|
514
516
|
bodyEmpty: 'Le corps du message ne peut pas être vide',
|
|
@@ -825,6 +827,7 @@ export default {
|
|
|
825
827
|
status: 'Statut',
|
|
826
828
|
updated_at: 'Mis à jour le',
|
|
827
829
|
},
|
|
830
|
+
loading: 'Chargement des messages...',
|
|
828
831
|
pagination_summary: '{from}–{to} sur {total}',
|
|
829
832
|
product_placeholder: 'Sélectionner un produit',
|
|
830
833
|
render_type_placeholder: 'Sélectionner le type de rendu',
|
package/i18n/locales/it.ts
CHANGED
|
@@ -507,6 +507,8 @@ export default {
|
|
|
507
507
|
},
|
|
508
508
|
validation: {
|
|
509
509
|
character_limit: 'Questo campo ha un limite di caratteri.',
|
|
510
|
+
header_line_breaks:
|
|
511
|
+
"Gli a capo non sono supportati nell'intestazione: le righe verranno unite in una sola.",
|
|
510
512
|
error: {
|
|
511
513
|
atLeastOneBlockRequired: 'È obbligatorio almeno un blocco di contenuto',
|
|
512
514
|
bodyEmpty: 'Il corpo del messaggio non può essere vuoto',
|
|
@@ -818,6 +820,7 @@ export default {
|
|
|
818
820
|
status: 'Stato',
|
|
819
821
|
updated_at: 'Aggiornato il',
|
|
820
822
|
},
|
|
823
|
+
loading: 'Caricamento dei messaggi...',
|
|
821
824
|
pagination_summary: '{from}–{to} di {total}',
|
|
822
825
|
product_placeholder: 'Seleziona prodotto',
|
|
823
826
|
render_type_placeholder: 'Seleziona tipo di rendering',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dev.smartpricing/message-composer-layer",
|
|
3
|
-
"version": "4.5.1-next.
|
|
3
|
+
"version": "4.5.1-next.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./nuxt.config.ts",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"vue-router": "^5.0.7",
|
|
36
36
|
"vue3-emoji-picker": "^1.1.8",
|
|
37
37
|
"zod": "^4.4.3",
|
|
38
|
-
"@dev.smartpricing/message-composer-utils": "4.5.1-next.
|
|
38
|
+
"@dev.smartpricing/message-composer-utils": "4.5.1-next.2"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@dev.smartpricing/platform-layer": "^0.0.23",
|