@dev.smartpricing/message-composer-layer 1.0.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/README.md +75 -0
- package/app/api/ai.ts +28 -0
- package/app/api/brands.ts +54 -0
- package/app/api/client.ts +56 -0
- package/app/api/compilation.ts +34 -0
- package/app/api/dynamicValues.ts +11 -0
- package/app/api/index.ts +11 -0
- package/app/api/languages.ts +6 -0
- package/app/api/media.ts +26 -0
- package/app/api/messageGroups.ts +49 -0
- package/app/api/messages.ts +22 -0
- package/app/api/meta.ts +18 -0
- package/app/api/tracking.ts +9 -0
- package/app/components/Brand/BrandDrawer.vue +281 -0
- package/app/components/Brand/BrandPreview.vue +137 -0
- package/app/components/Brand/Steps/BrandStepGeneral.vue +37 -0
- package/app/components/Brand/Steps/BrandStepSocials.vue +48 -0
- package/app/components/Brand/Steps/BrandStepVisual.vue +214 -0
- package/app/components/Common/BrandColorSwatches.vue +37 -0
- package/app/components/Common/ColorInput.vue +144 -0
- package/app/components/Common/EmojiPickerButton.vue +35 -0
- package/app/components/Common/FontFamilySelect.vue +39 -0
- package/app/components/Common/HtmlPreviewIframe.vue +68 -0
- package/app/components/Common/IncompleteTranslationsDialog.vue +97 -0
- package/app/components/Common/LanguageList.vue +356 -0
- package/app/components/Common/LoadingOverlay.vue +12 -0
- package/app/components/Common/MediaUploadOrSelect.vue +299 -0
- package/app/components/Common/MessagePreviewSlideover.vue +88 -0
- package/app/components/Common/MonacoEditor.vue +110 -0
- package/app/components/Common/PhoneInput.vue +804 -0
- package/app/components/Common/SectionHeader.vue +26 -0
- package/app/components/Common/TranslationActionBar.vue +139 -0
- package/app/components/Common/ViewLessMore.vue +40 -0
- package/app/components/CreateTemplateButton.vue +59 -0
- package/app/components/DynamicDataButton.vue +75 -0
- package/app/components/DynamicDataSlideover.vue +149 -0
- package/app/components/Editor/AiGenerateSlideover.vue +292 -0
- package/app/components/Editor/EditorLinkPopover.vue +154 -0
- package/app/components/Editor/RichTextEditor.vue +495 -0
- package/app/components/Email/Editor/BlockLibrary.vue +89 -0
- package/app/components/Email/Editor/BlockSettings.vue +60 -0
- package/app/components/Email/Editor/Blocks/ButtonBlock.vue +117 -0
- package/app/components/Email/Editor/Blocks/ButtonSettings.vue +228 -0
- package/app/components/Email/Editor/Blocks/DiscountCodeBlock.vue +74 -0
- package/app/components/Email/Editor/Blocks/DiscountCodeSettings.vue +162 -0
- package/app/components/Email/Editor/Blocks/DividerBlock.vue +31 -0
- package/app/components/Email/Editor/Blocks/DividerSettings.vue +108 -0
- package/app/components/Email/Editor/Blocks/FooterBlock.vue +61 -0
- package/app/components/Email/Editor/Blocks/FooterSettings.vue +123 -0
- package/app/components/Email/Editor/Blocks/GridBlock.vue +128 -0
- package/app/components/Email/Editor/Blocks/GridSettings.vue +109 -0
- package/app/components/Email/Editor/Blocks/HTMLBlock.vue +31 -0
- package/app/components/Email/Editor/Blocks/HTMLSettings.vue +85 -0
- package/app/components/Email/Editor/Blocks/HeaderBlock.vue +53 -0
- package/app/components/Email/Editor/Blocks/HeaderSettings.vue +113 -0
- package/app/components/Email/Editor/Blocks/HeadingBlock.vue +61 -0
- package/app/components/Email/Editor/Blocks/HeadingSettings.vue +156 -0
- package/app/components/Email/Editor/Blocks/ImageBlock.vue +49 -0
- package/app/components/Email/Editor/Blocks/ImageSettings.vue +53 -0
- package/app/components/Email/Editor/Blocks/IndexBlock.vue +155 -0
- package/app/components/Email/Editor/Blocks/ParagraphBlock.vue +61 -0
- package/app/components/Email/Editor/Blocks/ParagraphSettings.vue +157 -0
- package/app/components/Email/Editor/Blocks/SocialsBlock.vue +63 -0
- package/app/components/Email/Editor/Blocks/SocialsSettings.vue +117 -0
- package/app/components/Email/Editor/Blocks/SpacerBlock.vue +37 -0
- package/app/components/Email/Editor/Blocks/SpacerSettings.vue +95 -0
- package/app/components/Email/Editor/Blocks/TopProductsBlock.vue +47 -0
- package/app/components/Email/Editor/Blocks/TopProductsSettings.vue +87 -0
- package/app/components/Email/Editor/Blocks/UnsubscribeLinkBlock.vue +61 -0
- package/app/components/Email/Editor/Blocks/UnsubscribeLinkSettings.vue +123 -0
- package/app/components/Email/Editor/Canvas.vue +143 -0
- package/app/components/Email/Editor/GeneralStyleSettings.vue +125 -0
- package/app/components/Email/Editor/Index.vue +77 -0
- package/app/components/Email/Editor/LeftSidebar.vue +95 -0
- package/app/components/Email/InlinePreview.vue +124 -0
- package/app/components/Email/Preview.vue +126 -0
- package/app/components/Email/PreviewPanel.vue +57 -0
- package/app/components/Email/SendTestEmailModal.vue +121 -0
- package/app/components/Email/TranslationEditor.vue +198 -0
- package/app/components/Email/Translator/Button.vue +101 -0
- package/app/components/Email/Translator/Discount.vue +60 -0
- package/app/components/Email/Translator/Footer.vue +66 -0
- package/app/components/Email/Translator/Grid.vue +127 -0
- package/app/components/Email/Translator/Header.vue +90 -0
- package/app/components/Email/Translator/Heading.vue +66 -0
- package/app/components/Email/Translator/Image.vue +91 -0
- package/app/components/Email/Translator/Index.vue +138 -0
- package/app/components/Email/Translator/Paragraph.vue +66 -0
- package/app/components/Email/Translator/RawHtml.vue +64 -0
- package/app/components/Email/Translator/UnsubscribeLink.vue +59 -0
- package/app/components/TemplateList/BaseTable.vue +354 -0
- package/app/components/TemplateList/StatusCell.vue +67 -0
- package/app/components/Whatsapp/CategoryChanger.vue +63 -0
- package/app/components/Whatsapp/Editor/Body.vue +53 -0
- package/app/components/Whatsapp/Editor/Buttons.vue +341 -0
- package/app/components/Whatsapp/Editor/Footer.vue +70 -0
- package/app/components/Whatsapp/Editor/Header.vue +156 -0
- package/app/components/Whatsapp/Editor/Index.vue +89 -0
- package/app/components/Whatsapp/Editor/SyncWrapper.vue +166 -0
- package/app/components/Whatsapp/Preview.vue +197 -0
- package/app/components/Whatsapp/PreviewPanel.vue +24 -0
- package/app/components/Whatsapp/TranslationEditor.vue +150 -0
- package/app/components/Whatsapp/Translator/Body.vue +61 -0
- package/app/components/Whatsapp/Translator/Button.vue +209 -0
- package/app/components/Whatsapp/Translator/Footer.vue +56 -0
- package/app/components/Whatsapp/Translator/Header.vue +61 -0
- package/app/components/Whatsapp/Translator/HeaderMedia.vue +117 -0
- package/app/components/Whatsapp/Translator/Index.vue +159 -0
- package/app/composables/composerContext.ts +35 -0
- package/app/composables/useAutoTranslate.ts +242 -0
- package/app/composables/useAutoTranslateEmail.ts +294 -0
- package/app/composables/useChangeTracking.ts +212 -0
- package/app/composables/useChangeTrackingEmail.ts +227 -0
- package/app/composables/useChannels.ts +36 -0
- package/app/composables/useEmailBlockContext.ts +43 -0
- package/app/composables/useExitConfirmation.ts +39 -0
- package/app/composables/useGoBack.ts +15 -0
- package/app/composables/useIncompleteTranslations.ts +55 -0
- package/app/composables/useIncompleteTranslationsEmail.ts +56 -0
- package/app/composables/useMediaLimits.ts +42 -0
- package/app/composables/useTemplateFormValidation.ts +109 -0
- package/app/composables/useTemplateOperations.ts +147 -0
- package/app/composables/useTracking.ts +39 -0
- package/app/composables/useTranslationFilters.ts +176 -0
- package/app/composables/useTranslationFiltersEmail.ts +244 -0
- package/app/composables/useWhatsappTemplateBehaviors.ts +58 -0
- package/app/middleware/channel-guard.ts +13 -0
- package/app/pages/brands.vue +195 -0
- package/app/pages/images.vue +141 -0
- package/app/pages/index.vue +198 -0
- package/app/pages/preview/[id].vue +47 -0
- package/app/pages/template/email/[id].vue +596 -0
- package/app/pages/template/email/create.vue +562 -0
- package/app/pages/template/whatsapp/[id].vue +561 -0
- package/app/pages/template/whatsapp/create.vue +468 -0
- package/app/plugins/admin-shortcut.client.ts +7 -0
- package/app/plugins/tracking.client.ts +12 -0
- package/app/queries/ai.ts +20 -0
- package/app/queries/brands.ts +75 -0
- package/app/queries/compilation.ts +37 -0
- package/app/queries/dynamicValues.ts +13 -0
- package/app/queries/index.ts +9 -0
- package/app/queries/languages.ts +10 -0
- package/app/queries/media.ts +27 -0
- package/app/queries/messageGroups.ts +112 -0
- package/app/queries/messages.ts +42 -0
- package/app/queries/meta.ts +13 -0
- package/app/stores/adminStore.ts +50 -0
- package/app/stores/appContext.ts +99 -0
- package/app/stores/emailEditor.ts +220 -0
- package/app/stores/main.ts +3 -0
- package/app/stores/metaSubmission.ts +116 -0
- package/app/types/app-config.d.ts +14 -0
- package/app/types/hooks.d.ts +9 -0
- package/app/types/tracking.ts +148 -0
- package/app/utils/locale.ts +17 -0
- package/app/utils/notifyParent.ts +7 -0
- package/app/utils/resolveDefaultLanguage.ts +14 -0
- package/app/utils/socialDefaults.ts +42 -0
- package/app/utils/testIds.const.ts +728 -0
- package/app.config.ts +5 -0
- package/i18n/check-translations.ts +56 -0
- package/i18n/locales/de.ts +834 -0
- package/i18n/locales/en.ts +826 -0
- package/i18n/locales/es.ts +833 -0
- package/i18n/locales/fr.ts +837 -0
- package/i18n/locales/it.ts +831 -0
- package/nuxt.config.ts +65 -0
- package/package.json +64 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import type {
|
|
3
|
+
WhatsappMessage,
|
|
4
|
+
WaButtonV1,
|
|
5
|
+
HeaderBlockV1,
|
|
6
|
+
} from '@dev.smartpricing/message-composer-utils/types'
|
|
7
|
+
import { toRaw } from 'vue'
|
|
8
|
+
import WhatsappEditor from './Index.vue'
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
/**
|
|
12
|
+
* All messages by language ID
|
|
13
|
+
*/
|
|
14
|
+
messages: Record<string, WhatsappMessage>
|
|
15
|
+
/**
|
|
16
|
+
* Main language ID to edit
|
|
17
|
+
*/
|
|
18
|
+
mainLanguageId: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const props = defineProps<Props>()
|
|
22
|
+
|
|
23
|
+
const model = defineModel<WhatsappMessage>({ required: true })
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Syncs button addition to all other languages
|
|
27
|
+
*/
|
|
28
|
+
function handleButtonAdded(button: WaButtonV1) {
|
|
29
|
+
for (const langId in props.messages) {
|
|
30
|
+
if (langId === props.mainLanguageId) continue
|
|
31
|
+
|
|
32
|
+
const targetMsg = props.messages[langId]
|
|
33
|
+
if (!targetMsg?.body?.buttons) continue
|
|
34
|
+
|
|
35
|
+
// Check if button already exists
|
|
36
|
+
const exists = targetMsg.body.buttons.buttons.some((btn) => btn.id === button.id)
|
|
37
|
+
if (exists) continue
|
|
38
|
+
|
|
39
|
+
// Add button with same structure but empty text for translation
|
|
40
|
+
const newButton: WaButtonV1 = {
|
|
41
|
+
...structuredClone(toRaw(button)),
|
|
42
|
+
text: '', // Empty for translation
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
targetMsg.body.buttons.buttons.push(newButton)
|
|
46
|
+
targetMsg.body.buttons.enabled = targetMsg.body.buttons.buttons.length > 0
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Syncs button removal to all other languages
|
|
52
|
+
*/
|
|
53
|
+
function handleButtonRemoved(buttonId: string) {
|
|
54
|
+
for (const langId in props.messages) {
|
|
55
|
+
if (langId === props.mainLanguageId) continue
|
|
56
|
+
|
|
57
|
+
const targetMsg = props.messages[langId]
|
|
58
|
+
if (!targetMsg?.body?.buttons) continue
|
|
59
|
+
|
|
60
|
+
// Remove button by ID
|
|
61
|
+
targetMsg.body.buttons.buttons = targetMsg.body.buttons.buttons.filter(
|
|
62
|
+
(btn) => btn.id !== buttonId,
|
|
63
|
+
)
|
|
64
|
+
targetMsg.body.buttons.enabled = targetMsg.body.buttons.buttons.length > 0
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Syncs button type change to all other languages
|
|
70
|
+
*/
|
|
71
|
+
function handleButtonTypeChanged(button: WaButtonV1) {
|
|
72
|
+
for (const langId in props.messages) {
|
|
73
|
+
if (langId === props.mainLanguageId) continue
|
|
74
|
+
|
|
75
|
+
const targetMsg = props.messages[langId]
|
|
76
|
+
if (!targetMsg?.body?.buttons) continue
|
|
77
|
+
|
|
78
|
+
// Find matching button by ID and update type/structure
|
|
79
|
+
const index = targetMsg.body.buttons.buttons.findIndex((btn) => btn.id === button.id)
|
|
80
|
+
if (index === -1) continue
|
|
81
|
+
|
|
82
|
+
const existingButton = targetMsg.body.buttons.buttons[index]
|
|
83
|
+
if (!existingButton) continue
|
|
84
|
+
|
|
85
|
+
// Update button structure while preserving translated text
|
|
86
|
+
const updatedButton: WaButtonV1 = {
|
|
87
|
+
...structuredClone(toRaw(button)),
|
|
88
|
+
text: existingButton.text, // Keep translated text
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
targetMsg.body.buttons.buttons.splice(index, 1, updatedButton)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Syncs header type change to all other languages.
|
|
97
|
+
* Always overrides target header with correct structure for the new type.
|
|
98
|
+
*/
|
|
99
|
+
function handleHeaderTypeChanged(header: HeaderBlockV1) {
|
|
100
|
+
for (const langId in props.messages) {
|
|
101
|
+
if (langId === props.mainLanguageId) continue
|
|
102
|
+
|
|
103
|
+
const targetMsg = props.messages[langId]
|
|
104
|
+
if (!targetMsg?.body) continue
|
|
105
|
+
|
|
106
|
+
if (header.type === 'none') {
|
|
107
|
+
targetMsg.body.header = { type: 'none' }
|
|
108
|
+
} else if (header.type === 'text') {
|
|
109
|
+
targetMsg.body.header = { type: 'text', content: undefined }
|
|
110
|
+
} else {
|
|
111
|
+
// Media types: copy url and smartchatMediaId from base
|
|
112
|
+
targetMsg.body.header = structuredClone(toRaw(header))
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Syncs header media (url/metaMediaId) to all other languages.
|
|
119
|
+
* Always overrides all languages with the new media.
|
|
120
|
+
*/
|
|
121
|
+
function handleHeaderMediaChanged(payload: { header: HeaderBlockV1; previousUrl: string }) {
|
|
122
|
+
if (payload.header.type === 'none' || payload.header.type === 'text') return
|
|
123
|
+
|
|
124
|
+
for (const langId in props.messages) {
|
|
125
|
+
if (langId === props.mainLanguageId) continue
|
|
126
|
+
|
|
127
|
+
const targetMsg = props.messages[langId]
|
|
128
|
+
if (!targetMsg?.body?.header) continue
|
|
129
|
+
if (targetMsg.body.header.type !== payload.header.type) continue
|
|
130
|
+
|
|
131
|
+
targetMsg.body.header = structuredClone(toRaw(payload.header))
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Syncs footer enabled state to all other languages
|
|
137
|
+
*/
|
|
138
|
+
function handleFooterEnabledChanged(enabled: boolean) {
|
|
139
|
+
for (const langId in props.messages) {
|
|
140
|
+
if (langId === props.mainLanguageId) continue
|
|
141
|
+
|
|
142
|
+
const targetMsg = props.messages[langId]
|
|
143
|
+
if (!targetMsg?.body?.footer) continue
|
|
144
|
+
|
|
145
|
+
targetMsg.body.footer.enabled = enabled
|
|
146
|
+
|
|
147
|
+
// Clear content when disabled
|
|
148
|
+
if (!enabled) {
|
|
149
|
+
targetMsg.body.footer.content = ''
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
</script>
|
|
154
|
+
|
|
155
|
+
<template>
|
|
156
|
+
<WhatsappEditor
|
|
157
|
+
v-model="model"
|
|
158
|
+
@button-added="handleButtonAdded"
|
|
159
|
+
@button-removed="handleButtonRemoved"
|
|
160
|
+
@button-type-changed="handleButtonTypeChanged"
|
|
161
|
+
@header-type-changed="handleHeaderTypeChanged"
|
|
162
|
+
@header-media-changed="handleHeaderMediaChanged"
|
|
163
|
+
@footer-enabled-changed="handleFooterEnabledChanged"
|
|
164
|
+
:data-testid="whatsappEditorSyncWrapperTestIds.container"
|
|
165
|
+
/>
|
|
166
|
+
</template>
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import type {
|
|
3
|
+
PostEntity,
|
|
4
|
+
WaButtonTypeV1,
|
|
5
|
+
WhatsappMessage,
|
|
6
|
+
} from '@dev.smartpricing/message-composer-utils/types'
|
|
7
|
+
import { generateHTML } from '@tiptap/vue-3'
|
|
8
|
+
import { computed } from 'vue'
|
|
9
|
+
import { getTipTapExtensions } from '@dev.smartpricing/message-composer-utils/utils'
|
|
10
|
+
|
|
11
|
+
defineOptions({
|
|
12
|
+
name: 'WhatsappPreview',
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
const props = defineProps<{
|
|
16
|
+
message: PostEntity<WhatsappMessage>
|
|
17
|
+
dynamicData?: Record<string, any>
|
|
18
|
+
}>()
|
|
19
|
+
|
|
20
|
+
function replaceMentionsWithValues(html: string): string {
|
|
21
|
+
if (!props.dynamicData) return html
|
|
22
|
+
|
|
23
|
+
let replacedHtml = html
|
|
24
|
+
for (const [key, value] of Object.entries(props.dynamicData)) {
|
|
25
|
+
const mentionRegex = new RegExp(`{{${key}}}`, 'g')
|
|
26
|
+
replacedHtml = replacedHtml.replace(mentionRegex, value || `{{${key}}}`)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return replacedHtml
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const bodyHtml = computed(() => {
|
|
33
|
+
if (!props.message.body.body.content) return ''
|
|
34
|
+
const html = generateHTML(
|
|
35
|
+
props.message.body.body.content,
|
|
36
|
+
getTipTapExtensions({ preset: 'whatsapp' }),
|
|
37
|
+
).replaceAll('<p></p>', '<p> </p>')
|
|
38
|
+
return replaceMentionsWithValues(html)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
const headerHtml = computed(() => {
|
|
42
|
+
if (props.message.body.header.type !== 'text') return ''
|
|
43
|
+
if (!props.message.body.header.content) return ''
|
|
44
|
+
const html = generateHTML(
|
|
45
|
+
props.message.body.header.content,
|
|
46
|
+
getTipTapExtensions({ preset: 'whatsapp-header' }),
|
|
47
|
+
).replaceAll('<p></p>', '<p> </p>')
|
|
48
|
+
return replaceMentionsWithValues(html)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
const currentTime = computed(() => {
|
|
52
|
+
const now = new Date()
|
|
53
|
+
return now.toLocaleTimeString('en-US', {
|
|
54
|
+
hour: '2-digit',
|
|
55
|
+
minute: '2-digit',
|
|
56
|
+
hour12: true,
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
const hasHeader = computed(() => props.message.body.header.type !== 'none')
|
|
61
|
+
const hasFooter = computed(
|
|
62
|
+
() => props.message.body.footer.enabled && props.message.body.footer.content,
|
|
63
|
+
)
|
|
64
|
+
const hasButtons = computed(
|
|
65
|
+
() => props.message.body.buttons.enabled && props.message.body.buttons.buttons.length > 0,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
const getIcon = (type: WaButtonTypeV1) => {
|
|
69
|
+
if (type === 'unsubscribe') return 'ph:arrow-line-up-right'
|
|
70
|
+
if (type === 'link') return 'ph:arrow-line-up-right'
|
|
71
|
+
if (type === 'phone') return 'ph:phone-fill'
|
|
72
|
+
return null
|
|
73
|
+
}
|
|
74
|
+
</script>
|
|
75
|
+
|
|
76
|
+
<template>
|
|
77
|
+
<div
|
|
78
|
+
class="w-full flex flex-col items-center justify-start p-4"
|
|
79
|
+
:data-testid="whatsappPreviewTestIds.container"
|
|
80
|
+
>
|
|
81
|
+
<div
|
|
82
|
+
class="bg-[#f3edea] rounded border-4 border-transparent p-4 w-full max-w-[306px] min-h-[400px] relative"
|
|
83
|
+
:data-testid="whatsappPreviewTestIds.chatWindow"
|
|
84
|
+
>
|
|
85
|
+
<!-- Chat Bubble Container -->
|
|
86
|
+
<div class="flex flex-col items-end w-full">
|
|
87
|
+
<!-- Message Bubble -->
|
|
88
|
+
<div class="flex items-end justify-start w-full shadow-sm">
|
|
89
|
+
<!-- Speech Bubble Tail -->
|
|
90
|
+
<div class="flex items-center justify-center flex-shrink-0">
|
|
91
|
+
<div class="w-2 h-4 transform rotate-180 scale-y-[-1]">
|
|
92
|
+
<svg
|
|
93
|
+
viewBox="0 0 8 15"
|
|
94
|
+
fill="none"
|
|
95
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
96
|
+
class="w-full h-full"
|
|
97
|
+
>
|
|
98
|
+
<path d="M0 0v15c0-8.284 6.716-15 15-15H0z" fill="white" />
|
|
99
|
+
</svg>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<!-- Message Content -->
|
|
104
|
+
<div class="bg-white rounded-md p-1 flex-1 min-w-0">
|
|
105
|
+
<div class="flex flex-col w-full">
|
|
106
|
+
<!-- Message Content Section -->
|
|
107
|
+
<div class="flex flex-col p-1.5 pb-0">
|
|
108
|
+
<!-- Header Content (if exists) -->
|
|
109
|
+
<div v-if="hasHeader" class="mb-2" :data-testid="whatsappPreviewTestIds.header">
|
|
110
|
+
<div
|
|
111
|
+
v-if="message.body.header.type === 'text'"
|
|
112
|
+
class="text-sm text-primary-950 break-all"
|
|
113
|
+
>
|
|
114
|
+
<p v-html="headerHtml" />
|
|
115
|
+
</div>
|
|
116
|
+
<div v-else-if="message.body.header.type === 'image'" class="mb-2">
|
|
117
|
+
<img
|
|
118
|
+
v-show="message.body.header.url"
|
|
119
|
+
:src="message.body.header.url"
|
|
120
|
+
:alt="$t('editor.whatsapp.preview.header_image_alt')"
|
|
121
|
+
class="max-w-full h-auto rounded"
|
|
122
|
+
/>
|
|
123
|
+
</div>
|
|
124
|
+
<div v-else-if="message.body.header.type === 'video'" class="mb-2">
|
|
125
|
+
<video
|
|
126
|
+
v-show="message.body.header.url"
|
|
127
|
+
:src="message.body.header.url"
|
|
128
|
+
controls
|
|
129
|
+
class="max-w-full h-auto rounded"
|
|
130
|
+
/>
|
|
131
|
+
</div>
|
|
132
|
+
<div v-else-if="message.body.header.type === 'document'" class="mb-2">
|
|
133
|
+
<a
|
|
134
|
+
v-show="message.body.header.url"
|
|
135
|
+
:href="message.body.header.url"
|
|
136
|
+
class="text-blue-600 underline text-sm"
|
|
137
|
+
>
|
|
138
|
+
📎 {{ $t('editor.whatsapp.preview.document_link') }}
|
|
139
|
+
</a>
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
<!-- Body Content -->
|
|
144
|
+
<div
|
|
145
|
+
class="text-sm text-primary-950 leading-[18px] break-all"
|
|
146
|
+
v-html="bodyHtml"
|
|
147
|
+
:data-testid="whatsappPreviewTestIds.body"
|
|
148
|
+
/>
|
|
149
|
+
|
|
150
|
+
<!-- Footer Content -->
|
|
151
|
+
<div
|
|
152
|
+
v-if="hasFooter"
|
|
153
|
+
class="mt-1 pt-0.5"
|
|
154
|
+
:data-testid="whatsappPreviewTestIds.footer"
|
|
155
|
+
>
|
|
156
|
+
<div class="text-sm text-primary-600 leading-[18px]">
|
|
157
|
+
{{ message.body.footer.content }}
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
<!-- Timestamp -->
|
|
163
|
+
<div class="px-1.5 pb-1 pt-2">
|
|
164
|
+
<div class="text-xs text-primary-600 text-right leading-3">
|
|
165
|
+
{{ currentTime }}
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
<div
|
|
173
|
+
v-if="hasButtons"
|
|
174
|
+
class="mt-2 ml-2"
|
|
175
|
+
:data-testid="whatsappPreviewTestIds.buttonsContainer"
|
|
176
|
+
>
|
|
177
|
+
<div class="flex flex-col gap-2">
|
|
178
|
+
<div
|
|
179
|
+
v-for="button in message.body.buttons.buttons"
|
|
180
|
+
:key="button.id"
|
|
181
|
+
class="p-2 bg-white text-[#0096DE] flex items-center justify-center rounded-md text-sm gap-1"
|
|
182
|
+
:data-testid="`${whatsappPreviewTestIds.button}-${button.id}`"
|
|
183
|
+
>
|
|
184
|
+
<UIcon
|
|
185
|
+
v-if="getIcon(button.type)"
|
|
186
|
+
:name="getIcon(button.type) ?? ''"
|
|
187
|
+
:size="16"
|
|
188
|
+
class="fill-current"
|
|
189
|
+
/>
|
|
190
|
+
|
|
191
|
+
{{ button.text }}
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
</template>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import type { PostEntity, WhatsappMessage } from '@dev.smartpricing/message-composer-utils/types'
|
|
3
|
+
import WhatsappPreview from './Preview.vue'
|
|
4
|
+
|
|
5
|
+
defineOptions({
|
|
6
|
+
name: 'WhatsappPreviewPanel',
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
defineProps<{
|
|
10
|
+
message: PostEntity<WhatsappMessage>
|
|
11
|
+
dynamicData?: Record<string, any>
|
|
12
|
+
}>()
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<template>
|
|
16
|
+
<div class="bg-white flex flex-col w-full" :data-testid="whatsappPreviewPanelTestIds.container">
|
|
17
|
+
<!-- Header Section -->
|
|
18
|
+
<div class="flex items-center justify-between p-4 pb-0">
|
|
19
|
+
<h3 class="label-lg">{{ $t('common.preview') }}</h3>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<WhatsappPreview :message="message" :dynamic-data="dynamicData" />
|
|
23
|
+
</div>
|
|
24
|
+
</template>
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, computed, watch } from 'vue'
|
|
3
|
+
import type { WhatsappMessage, PostEntity } from '@dev.smartpricing/message-composer-utils/types'
|
|
4
|
+
import WhatsappTranslator from '@/components/Whatsapp/Translator/Index.vue'
|
|
5
|
+
import TranslationActionBar from '@/components/Common/TranslationActionBar.vue'
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
baseMessage: WhatsappMessage | PostEntity<WhatsappMessage>
|
|
9
|
+
messages: Record<string, PostEntity<WhatsappMessage>>
|
|
10
|
+
currentLanguage: string
|
|
11
|
+
baseLanguage: string
|
|
12
|
+
messageGroupId?: string | number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const props = defineProps<Props>()
|
|
16
|
+
|
|
17
|
+
const emit = defineEmits<{
|
|
18
|
+
'auto-translate-tracked': [language: string]
|
|
19
|
+
preview: [language: string]
|
|
20
|
+
'filters-changed': [
|
|
21
|
+
payload: {
|
|
22
|
+
language: string
|
|
23
|
+
search_term: string
|
|
24
|
+
show_only_missing: boolean
|
|
25
|
+
show_only_changes: boolean
|
|
26
|
+
},
|
|
27
|
+
]
|
|
28
|
+
}>()
|
|
29
|
+
|
|
30
|
+
const targetMessage = defineModel<WhatsappMessage | PostEntity<WhatsappMessage>>('targetMessage', {
|
|
31
|
+
required: true,
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
// Filter states
|
|
35
|
+
const searchQuery = ref('')
|
|
36
|
+
const showOnlyMissing = ref(false)
|
|
37
|
+
const showOnlyChanges = ref(false)
|
|
38
|
+
|
|
39
|
+
// Auto-translate composable
|
|
40
|
+
const { translateMessage, isTranslating, error } = useAutoTranslate()
|
|
41
|
+
|
|
42
|
+
// Filters composable
|
|
43
|
+
const messagesRef = computed(() => props.messages)
|
|
44
|
+
const currentLanguageRef = computed(() => props.currentLanguage)
|
|
45
|
+
const baseLanguageRef = computed(() => props.baseLanguage)
|
|
46
|
+
const messageGroupIdRef = computed(() => props.messageGroupId)
|
|
47
|
+
|
|
48
|
+
const { filteredSections, hasActiveFilters } = useTranslationFilters({
|
|
49
|
+
messages: messagesRef,
|
|
50
|
+
currentLanguage: currentLanguageRef,
|
|
51
|
+
baseLanguage: baseLanguageRef,
|
|
52
|
+
messageGroupId: messageGroupIdRef,
|
|
53
|
+
searchQuery,
|
|
54
|
+
showOnlyMissing,
|
|
55
|
+
showOnlyChanges,
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
// Event handlers
|
|
59
|
+
async function handleAutoTranslate() {
|
|
60
|
+
const baseMsg = props.messages[props.baseLanguage]
|
|
61
|
+
const targetMsg = props.messages[props.currentLanguage]
|
|
62
|
+
|
|
63
|
+
if (!baseMsg || !targetMsg) {
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
emit('auto-translate-tracked', props.currentLanguage)
|
|
68
|
+
|
|
69
|
+
const success = await translateMessage(
|
|
70
|
+
baseMsg,
|
|
71
|
+
targetMsg,
|
|
72
|
+
props.baseLanguage,
|
|
73
|
+
props.currentLanguage,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
if (success) {
|
|
77
|
+
// Success notification will be handled by parent or toast
|
|
78
|
+
console.log('Translation completed successfully')
|
|
79
|
+
} else if (error.value) {
|
|
80
|
+
// Error notification will be handled by parent or toast
|
|
81
|
+
console.error('Translation failed:', error.value)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function handlePreview() {
|
|
86
|
+
emit('preview', props.currentLanguage)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Emit filters-changed when any filter changes
|
|
90
|
+
watch([searchQuery, showOnlyMissing, showOnlyChanges], () => {
|
|
91
|
+
emit('filters-changed', {
|
|
92
|
+
language: props.currentLanguage,
|
|
93
|
+
search_term: searchQuery.value,
|
|
94
|
+
show_only_missing: showOnlyMissing.value,
|
|
95
|
+
show_only_changes: showOnlyChanges.value,
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
function clearFilters() {
|
|
100
|
+
searchQuery.value = ''
|
|
101
|
+
showOnlyMissing.value = false
|
|
102
|
+
showOnlyChanges.value = false
|
|
103
|
+
}
|
|
104
|
+
</script>
|
|
105
|
+
|
|
106
|
+
<template>
|
|
107
|
+
<div class="flex flex-col relative" :data-testid="whatsappTranslationEditorTestIds.container">
|
|
108
|
+
<!-- Action Bar -->
|
|
109
|
+
<TranslationActionBar
|
|
110
|
+
:messages="messages"
|
|
111
|
+
:current-language="currentLanguage"
|
|
112
|
+
:base-language="baseLanguage"
|
|
113
|
+
:message-group-id="messageGroupId"
|
|
114
|
+
:is-translating="isTranslating"
|
|
115
|
+
v-model:search-query="searchQuery"
|
|
116
|
+
v-model:show-only-missing="showOnlyMissing"
|
|
117
|
+
v-model:show-only-changes="showOnlyChanges"
|
|
118
|
+
@auto-translate="handleAutoTranslate"
|
|
119
|
+
@preview="handlePreview"
|
|
120
|
+
/>
|
|
121
|
+
|
|
122
|
+
<!-- Empty State when no sections match filters -->
|
|
123
|
+
<UEmpty
|
|
124
|
+
v-if="hasActiveFilters && filteredSections.length === 0"
|
|
125
|
+
icon="ph:funnel"
|
|
126
|
+
:title="$t('translation_action_bar.no_sections_found')"
|
|
127
|
+
:data-testid="whatsappTranslationEditorTestIds.emptyState"
|
|
128
|
+
>
|
|
129
|
+
<template #actions>
|
|
130
|
+
<UButton
|
|
131
|
+
color="neutral"
|
|
132
|
+
variant="outline"
|
|
133
|
+
size="sm"
|
|
134
|
+
@click="clearFilters"
|
|
135
|
+
:data-testid="whatsappTranslationEditorTestIds.clearFiltersButton"
|
|
136
|
+
>
|
|
137
|
+
{{ $t('translation_action_bar.clear_filters') }}
|
|
138
|
+
</UButton>
|
|
139
|
+
</template>
|
|
140
|
+
</UEmpty>
|
|
141
|
+
|
|
142
|
+
<!-- Translator -->
|
|
143
|
+
<WhatsappTranslator
|
|
144
|
+
v-if="!hasActiveFilters || filteredSections.length > 0"
|
|
145
|
+
:base-message="baseMessage"
|
|
146
|
+
v-model:target-message="targetMessage"
|
|
147
|
+
:filtered-sections="hasActiveFilters ? filteredSections : undefined"
|
|
148
|
+
/>
|
|
149
|
+
</div>
|
|
150
|
+
</template>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { JSONContent } from '@tiptap/vue-3'
|
|
3
|
+
import { generateHTML } from '@tiptap/vue-3'
|
|
4
|
+
import { getTipTapExtensions } from '@dev.smartpricing/message-composer-utils/utils'
|
|
5
|
+
import RichTextEditor from '@/components/Editor/RichTextEditor.vue'
|
|
6
|
+
import { useAppContextStore } from '@/stores/appContext'
|
|
7
|
+
import { WA_BODY_MAX_LENGTH } from '@dev.smartpricing/message-composer-utils/types'
|
|
8
|
+
|
|
9
|
+
defineOptions({
|
|
10
|
+
name: 'WhatsappTranslatorBody',
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
const { baseContent } = defineProps<{
|
|
14
|
+
baseContent: JSONContent | undefined
|
|
15
|
+
}>()
|
|
16
|
+
|
|
17
|
+
const targetContent = defineModel<JSONContent | undefined>({
|
|
18
|
+
required: true,
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const appContext = useAppContextStore()
|
|
22
|
+
|
|
23
|
+
// Generate HTML for preview of base language
|
|
24
|
+
function generatePreviewHtml(content: JSONContent | undefined): string {
|
|
25
|
+
if (!content) return ''
|
|
26
|
+
return generateHTML(content, getTipTapExtensions({ preset: 'whatsapp' })).replaceAll(
|
|
27
|
+
'<p></p>',
|
|
28
|
+
'<p> </p>',
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<template>
|
|
34
|
+
<div class="flex gap-8 items-start" :data-testid="whatsappTranslatorBodyTestIds.container">
|
|
35
|
+
<!-- Base language preview (read-only) -->
|
|
36
|
+
<div class="flex-1 min-w-0" :data-testid="whatsappTranslatorBodyTestIds.basePreview">
|
|
37
|
+
<div
|
|
38
|
+
class="paragraph-sm text-primary-700 break-words"
|
|
39
|
+
v-html="generatePreviewHtml(baseContent)"
|
|
40
|
+
/>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<!-- Translation editor (editable) -->
|
|
44
|
+
<div class="flex-1 min-w-0 flex gap-2 items-start">
|
|
45
|
+
<div class="flex-1">
|
|
46
|
+
<RichTextEditor
|
|
47
|
+
v-model:text="targetContent"
|
|
48
|
+
preset="whatsapp"
|
|
49
|
+
dynamic-values
|
|
50
|
+
bold
|
|
51
|
+
italic
|
|
52
|
+
emoji
|
|
53
|
+
:character-limit="WA_BODY_MAX_LENGTH"
|
|
54
|
+
:data-testid="whatsappTranslatorBodyTestIds.targetEditor"
|
|
55
|
+
/>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<slot name="actions" />
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
</template>
|