@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,468 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
definePageMeta({
|
|
3
|
+
middleware: ['channel-guard'],
|
|
4
|
+
})
|
|
5
|
+
|
|
6
|
+
import WhatsappEditorSyncWrapper from '@/components/Whatsapp/Editor/SyncWrapper.vue'
|
|
7
|
+
import WhatsappPreview from '@/components/Whatsapp/PreviewPanel.vue'
|
|
8
|
+
import TranslationEditor from '@/components/Whatsapp/TranslationEditor.vue'
|
|
9
|
+
|
|
10
|
+
import IncompleteTranslationsDialog from '@/components/Common/IncompleteTranslationsDialog.vue'
|
|
11
|
+
|
|
12
|
+
import CategoryChanger from '@/components/Whatsapp/CategoryChanger.vue'
|
|
13
|
+
import { computed, ref, watch } from 'vue'
|
|
14
|
+
import {
|
|
15
|
+
type MessageGroup,
|
|
16
|
+
type MessageGroupCategory,
|
|
17
|
+
type PostEntity,
|
|
18
|
+
type WhatsappMessage,
|
|
19
|
+
type LanguageFromDb,
|
|
20
|
+
} from '@dev.smartpricing/message-composer-utils/types'
|
|
21
|
+
import { getWhatsappEmpty } from '@dev.smartpricing/message-composer-utils/utils'
|
|
22
|
+
import LanguageList from '@/components/Common/LanguageList.vue'
|
|
23
|
+
import { useMetaSubmissionStore } from '@/stores/metaSubmission'
|
|
24
|
+
import { useLanguagesQuery } from '~/queries'
|
|
25
|
+
import { useAppContextStore } from '@/stores/appContext'
|
|
26
|
+
import type { WaHeaderType } from '~/types/tracking'
|
|
27
|
+
|
|
28
|
+
const { trackEvent } = useTracking()
|
|
29
|
+
const { mainLanguage } = storeToRefs(useAppContextStore())
|
|
30
|
+
|
|
31
|
+
const adminStore = useAdminStore()
|
|
32
|
+
|
|
33
|
+
const name = ref('')
|
|
34
|
+
const category = ref<MessageGroupCategory>('marketing')
|
|
35
|
+
const isInternal = ref(false)
|
|
36
|
+
const language = ref(mainLanguage.value)
|
|
37
|
+
const { goBackOrFallback } = useGoBack()
|
|
38
|
+
|
|
39
|
+
const activeTab = ref<'content' | 'translations'>('content')
|
|
40
|
+
const baseLanguage = ref(mainLanguage.value)
|
|
41
|
+
const targetLanguage = ref(mainLanguage.value)
|
|
42
|
+
|
|
43
|
+
// Create temporary draft ID for change tracking
|
|
44
|
+
const draftId = ref(`draft-${Date.now()}`)
|
|
45
|
+
const changeTracking = useChangeTracking(draftId)
|
|
46
|
+
|
|
47
|
+
// Store original messageGroup values (empty for new message group)
|
|
48
|
+
const originalMessageGroupName = ref('')
|
|
49
|
+
const originalMessageGroupCategory = ref<MessageGroupCategory>('marketing')
|
|
50
|
+
|
|
51
|
+
const { saving, createTemplate } = useTemplateOperations({
|
|
52
|
+
onSuccess: () => {
|
|
53
|
+
created.value = true
|
|
54
|
+
changeTracking.clearAllChanges()
|
|
55
|
+
goBackOrFallback()
|
|
56
|
+
},
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
const { submitToMeta } = useMetaSubmissionStore()
|
|
60
|
+
|
|
61
|
+
const messageGroup = computed(() => {
|
|
62
|
+
return {
|
|
63
|
+
name: name.value,
|
|
64
|
+
category: category.value,
|
|
65
|
+
visibility: isInternal.value ? 'internal' : 'public',
|
|
66
|
+
} satisfies PostEntity<MessageGroup>
|
|
67
|
+
})
|
|
68
|
+
const messages = ref<Record<string, PostEntity<WhatsappMessage>>>({
|
|
69
|
+
[mainLanguage.value]: getWhatsappEmpty(mainLanguage.value),
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
const { formState, draftSchema, validationErrors, validateDraft, validateFull, clearErrors } =
|
|
73
|
+
useTemplateFormValidation({
|
|
74
|
+
type: 'whatsapp',
|
|
75
|
+
name,
|
|
76
|
+
category,
|
|
77
|
+
messages,
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
// Record original state for initial mainLanguage
|
|
81
|
+
if (messages.value[mainLanguage.value]) {
|
|
82
|
+
changeTracking.recordOriginalState(
|
|
83
|
+
mainLanguage.value,
|
|
84
|
+
messages.value[mainLanguage.value] as WhatsappMessage,
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Initialize WhatsApp message group behaviors composable
|
|
89
|
+
const { ensureUnsubscribeButton } = useWhatsappTemplateBehaviors({
|
|
90
|
+
category,
|
|
91
|
+
messages: messages,
|
|
92
|
+
})
|
|
93
|
+
ensureUnsubscribeButton()
|
|
94
|
+
|
|
95
|
+
const created = ref(false)
|
|
96
|
+
|
|
97
|
+
const hasAnyChanges = computed(() => {
|
|
98
|
+
// Check if message group name or category changed
|
|
99
|
+
const nameChanged = name.value !== originalMessageGroupName.value
|
|
100
|
+
const categoryChanged = category.value !== originalMessageGroupCategory.value
|
|
101
|
+
if (nameChanged || categoryChanged) return true
|
|
102
|
+
|
|
103
|
+
// Check if any language has changes
|
|
104
|
+
for (const languageId in messages.value) {
|
|
105
|
+
const message = messages.value[languageId]
|
|
106
|
+
if (message && changeTracking.hasChanges(languageId, message as WhatsappMessage)) {
|
|
107
|
+
return true
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return false
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
useExitConfirmation(computed(() => hasAnyChanges.value && !created.value))
|
|
114
|
+
|
|
115
|
+
// ── Tracking helpers ──
|
|
116
|
+
function getWaBaseProps() {
|
|
117
|
+
return { name: name.value, category: category.value }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function getWaDraftProps() {
|
|
121
|
+
const msg = messages.value[mainLanguage.value]
|
|
122
|
+
return {
|
|
123
|
+
...getWaBaseProps(),
|
|
124
|
+
languages: Object.keys(messages.value),
|
|
125
|
+
header: (msg?.body?.header?.type ?? 'none') as WaHeaderType,
|
|
126
|
+
footer: !!msg?.body?.footer?.enabled,
|
|
127
|
+
buttons: msg?.body?.buttons?.buttons?.length ?? 0,
|
|
128
|
+
is_internal: isInternal.value,
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
watch(activeTab, (tab) => {
|
|
133
|
+
trackEvent('wa_create.change_tab', { ...getWaBaseProps(), tab })
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
function onAddLanguage(lang: string) {
|
|
137
|
+
trackEvent('wa_create.add_language', { ...getWaBaseProps(), language: lang })
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function onRemoveLanguage(lang: string) {
|
|
141
|
+
trackEvent('wa_create.remove_language', {
|
|
142
|
+
...getWaBaseProps(),
|
|
143
|
+
language: lang,
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function onAutoTranslateTracked(lang: string) {
|
|
148
|
+
trackEvent('wa_create.auto_translate', {
|
|
149
|
+
...getWaBaseProps(),
|
|
150
|
+
language: lang,
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function onPreview(lang: string) {
|
|
155
|
+
trackEvent('wa_create.preview', { ...getWaBaseProps(), language: lang })
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function onFiltersChanged(payload: {
|
|
159
|
+
language: string
|
|
160
|
+
search_term: string
|
|
161
|
+
show_only_missing: boolean
|
|
162
|
+
show_only_changes: boolean
|
|
163
|
+
}) {
|
|
164
|
+
trackEvent('wa_create.translations_filters', { ...getWaBaseProps(), ...payload })
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Get languages for incomplete translations check
|
|
168
|
+
const { data: languages } = useLanguagesQuery()
|
|
169
|
+
|
|
170
|
+
// Check for incomplete translations
|
|
171
|
+
const { incompleteLanguages, hasIncompleteTranslations } = useIncompleteTranslations({
|
|
172
|
+
messages,
|
|
173
|
+
languages,
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
// Dialog state and pending action
|
|
177
|
+
const showIncompleteDialog = ref(false)
|
|
178
|
+
const pendingAction = ref<'save_draft' | 'request_approval' | null>(null)
|
|
179
|
+
|
|
180
|
+
// Auto-translate composable
|
|
181
|
+
const { translateMessage, isTranslating } = useAutoTranslate()
|
|
182
|
+
|
|
183
|
+
async function saveDraft() {
|
|
184
|
+
trackEvent('wa_create.save_draft', getWaDraftProps())
|
|
185
|
+
const templateData = {
|
|
186
|
+
messageGroup: messageGroup.value,
|
|
187
|
+
messages: messages.value,
|
|
188
|
+
}
|
|
189
|
+
const result = await createTemplate(templateData, 'draft')
|
|
190
|
+
if (result) {
|
|
191
|
+
notifyParent('TEMPLATE_SAVED_DRAFT', { id: result.id, name: result.name })
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function requestApproval() {
|
|
196
|
+
trackEvent('wa_create.request_approval', getWaDraftProps())
|
|
197
|
+
const templateData = {
|
|
198
|
+
messageGroup: messageGroup.value,
|
|
199
|
+
messages: messages.value,
|
|
200
|
+
}
|
|
201
|
+
const result = await createTemplate(templateData, 'draft')
|
|
202
|
+
|
|
203
|
+
if (result) {
|
|
204
|
+
await submitToMeta(result.id.toString())
|
|
205
|
+
notifyParent('TEMPLATE_APPROVAL_REQUESTED', { id: result.id, name: result.name })
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Wrapper functions to validate then check for incomplete translations
|
|
210
|
+
async function handleSaveDraft() {
|
|
211
|
+
if (!(await validateDraft())) return
|
|
212
|
+
if (hasIncompleteTranslations.value) {
|
|
213
|
+
pendingAction.value = 'save_draft'
|
|
214
|
+
showIncompleteDialog.value = true
|
|
215
|
+
} else {
|
|
216
|
+
saveDraft()
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function handleRequestApproval() {
|
|
221
|
+
if (!(await validateFull())) return
|
|
222
|
+
if (hasIncompleteTranslations.value) {
|
|
223
|
+
pendingAction.value = 'request_approval'
|
|
224
|
+
showIncompleteDialog.value = true
|
|
225
|
+
} else {
|
|
226
|
+
requestApproval()
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Proceed with save without translating
|
|
231
|
+
function proceedWithSave() {
|
|
232
|
+
if (pendingAction.value === 'save_draft') {
|
|
233
|
+
saveDraft()
|
|
234
|
+
} else if (pendingAction.value === 'request_approval') {
|
|
235
|
+
requestApproval()
|
|
236
|
+
}
|
|
237
|
+
pendingAction.value = null
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Go to translations tab
|
|
241
|
+
function goToTranslations() {
|
|
242
|
+
activeTab.value = 'translations'
|
|
243
|
+
pendingAction.value = null
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Translate automatically and save
|
|
247
|
+
async function translateAndSave() {
|
|
248
|
+
if (!incompleteLanguages.value.length) {
|
|
249
|
+
proceedWithSave()
|
|
250
|
+
return
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const baseMessage = messages.value[baseLanguage.value]
|
|
254
|
+
if (!baseMessage) {
|
|
255
|
+
proceedWithSave()
|
|
256
|
+
return
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Translate all incomplete languages
|
|
260
|
+
for (const { language: lang } of incompleteLanguages.value) {
|
|
261
|
+
const targetMessage = messages.value[lang.id]
|
|
262
|
+
if (!targetMessage) continue
|
|
263
|
+
|
|
264
|
+
await translateMessage(
|
|
265
|
+
baseMessage as WhatsappMessage,
|
|
266
|
+
targetMessage as WhatsappMessage,
|
|
267
|
+
baseLanguage.value,
|
|
268
|
+
lang.id,
|
|
269
|
+
)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// After translation, proceed with save
|
|
273
|
+
proceedWithSave()
|
|
274
|
+
}
|
|
275
|
+
</script>
|
|
276
|
+
|
|
277
|
+
<template>
|
|
278
|
+
<SNavigationPage>
|
|
279
|
+
<template #header>
|
|
280
|
+
<SNavigationBarHeader
|
|
281
|
+
:title="$t('pages.whatsapp.create_message_group.title')"
|
|
282
|
+
:tabs="[
|
|
283
|
+
{ label: $t('pages.whatsapp.tabs.content'), value: 'content' },
|
|
284
|
+
{ label: $t('pages.whatsapp.tabs.translations'), value: 'translations' },
|
|
285
|
+
]"
|
|
286
|
+
:active-tab="activeTab"
|
|
287
|
+
:data-testid="templateWhatsappCreateTestIds.navbar"
|
|
288
|
+
@tab-change="activeTab = $event"
|
|
289
|
+
>
|
|
290
|
+
<template #actions>
|
|
291
|
+
<UButton
|
|
292
|
+
@click="
|
|
293
|
+
() => {
|
|
294
|
+
notifyParent('TEMPLATE_CANCELLED')
|
|
295
|
+
goBackOrFallback()
|
|
296
|
+
}
|
|
297
|
+
"
|
|
298
|
+
:label="$t('common.actions.cancel')"
|
|
299
|
+
color="primary"
|
|
300
|
+
variant="ghost"
|
|
301
|
+
:loading="saving || isTranslating"
|
|
302
|
+
:data-testid="templateWhatsappCreateTestIds.cancelButton"
|
|
303
|
+
/>
|
|
304
|
+
<UButton
|
|
305
|
+
@click="handleSaveDraft()"
|
|
306
|
+
:label="$t('pages.whatsapp.create_message_group.save_draft')"
|
|
307
|
+
color="primary"
|
|
308
|
+
variant="outline"
|
|
309
|
+
:loading="saving || isTranslating"
|
|
310
|
+
:data-testid="templateWhatsappCreateTestIds.saveDraftButton"
|
|
311
|
+
/>
|
|
312
|
+
<UButton
|
|
313
|
+
@click="handleRequestApproval()"
|
|
314
|
+
:label="$t('pages.whatsapp.create_message_group.request_approval')"
|
|
315
|
+
color="primary"
|
|
316
|
+
:loading="saving || isTranslating"
|
|
317
|
+
:data-testid="templateWhatsappCreateTestIds.requestApprovalButton"
|
|
318
|
+
/>
|
|
319
|
+
</template>
|
|
320
|
+
</SNavigationBarHeader>
|
|
321
|
+
</template>
|
|
322
|
+
|
|
323
|
+
<div v-if="validationErrors.length">
|
|
324
|
+
<UAlert
|
|
325
|
+
color="error"
|
|
326
|
+
variant="subtle"
|
|
327
|
+
icon="i-lucide-alert-triangle"
|
|
328
|
+
:title="$t('editor.validation.error.title')"
|
|
329
|
+
:close="true"
|
|
330
|
+
@update:open="clearErrors()"
|
|
331
|
+
:data-testid="templateWhatsappCreateTestIds.validationAlert"
|
|
332
|
+
>
|
|
333
|
+
<template #description>
|
|
334
|
+
<ul class="list-disc pl-4 space-y-1">
|
|
335
|
+
<li v-for="err in validationErrors" :key="err">
|
|
336
|
+
{{ err }}
|
|
337
|
+
</li>
|
|
338
|
+
</ul>
|
|
339
|
+
</template>
|
|
340
|
+
</UAlert>
|
|
341
|
+
</div>
|
|
342
|
+
|
|
343
|
+
<UForm
|
|
344
|
+
ref="templateForm"
|
|
345
|
+
:schema="draftSchema"
|
|
346
|
+
:state="formState"
|
|
347
|
+
:validate-on="[]"
|
|
348
|
+
class="flex-1 relative min-h-0 flex flex-col"
|
|
349
|
+
>
|
|
350
|
+
<UCard
|
|
351
|
+
v-if="activeTab === 'content'"
|
|
352
|
+
:ui="{
|
|
353
|
+
root: 'flex flex-col',
|
|
354
|
+
body: 'flex-1 overflow-hidden sm:px-0 px-0 py-0 sm:py-0',
|
|
355
|
+
header: 'sm:px-4 px-4 py-4 sm:py-4',
|
|
356
|
+
}"
|
|
357
|
+
:data-testid="templateWhatsappCreateTestIds.contentCard"
|
|
358
|
+
>
|
|
359
|
+
<template #header>
|
|
360
|
+
<div class="grid grid-cols-3 gap-2">
|
|
361
|
+
<UFieldGroup>
|
|
362
|
+
<UBadge variant="outline" color="neutral" size="lg">{{
|
|
363
|
+
$t('pages.whatsapp.create_message_group.name')
|
|
364
|
+
}}</UBadge>
|
|
365
|
+
<UInput
|
|
366
|
+
v-model="name"
|
|
367
|
+
:placeholder="$t('pages.whatsapp.create_message_group.name_placeholder')"
|
|
368
|
+
:data-testid="templateWhatsappCreateTestIds.nameInput"
|
|
369
|
+
/>
|
|
370
|
+
</UFieldGroup>
|
|
371
|
+
|
|
372
|
+
<UFieldGroup>
|
|
373
|
+
<UBadge variant="outline" color="neutral" size="lg">{{
|
|
374
|
+
$t('pages.whatsapp.create_message_group.category')
|
|
375
|
+
}}</UBadge>
|
|
376
|
+
<CategoryChanger v-model:category="category" @changed="ensureUnsubscribeButton" />
|
|
377
|
+
</UFieldGroup>
|
|
378
|
+
|
|
379
|
+
<USwitch
|
|
380
|
+
v-model="isInternal"
|
|
381
|
+
:label="$t('common.internal')"
|
|
382
|
+
v-if="adminStore.isAdminModeActive"
|
|
383
|
+
color="info"
|
|
384
|
+
:data-testid="templateWhatsappCreateTestIds.internalSwitch"
|
|
385
|
+
/>
|
|
386
|
+
<!-- <DocumentationPanel
|
|
387
|
+
:category="$t('documentation.whatsapp_category.category')"
|
|
388
|
+
:title="$t('documentation.whatsapp_category.title')"
|
|
389
|
+
:description="$t('documentation.whatsapp_category.description')"
|
|
390
|
+
:info="{
|
|
391
|
+
title: '',
|
|
392
|
+
text: $t('documentation.whatsapp_category.info'),
|
|
393
|
+
}"
|
|
394
|
+
:content="[
|
|
395
|
+
$t('documentation.whatsapp_category.content.marketing'),
|
|
396
|
+
$t('documentation.whatsapp_category.content.utility'),
|
|
397
|
+
]"
|
|
398
|
+
/> -->
|
|
399
|
+
</div>
|
|
400
|
+
</template>
|
|
401
|
+
|
|
402
|
+
<div class="grid grid-cols-3 divide-x divide-muted overflow-auto max-h-full h-full">
|
|
403
|
+
<div class="col-span-2 overflow-y-auto pt-4 pb-4" v-if="messages[language]">
|
|
404
|
+
<h2 class="px-4 pb-4 label-lg">
|
|
405
|
+
{{ $t('pages.whatsapp.create_message_group.message_content') }}
|
|
406
|
+
</h2>
|
|
407
|
+
<WhatsappEditorSyncWrapper
|
|
408
|
+
v-model="messages[language]"
|
|
409
|
+
:messages="messages"
|
|
410
|
+
:main-language-id="language"
|
|
411
|
+
:key="language"
|
|
412
|
+
/>
|
|
413
|
+
</div>
|
|
414
|
+
<div class="col-span-1 overflow-y-auto" v-if="messages[language]">
|
|
415
|
+
<WhatsappPreview :message="messages[language]" />
|
|
416
|
+
</div>
|
|
417
|
+
</div>
|
|
418
|
+
</UCard>
|
|
419
|
+
|
|
420
|
+
<UCard
|
|
421
|
+
v-if="activeTab === 'translations'"
|
|
422
|
+
:ui="{
|
|
423
|
+
root: 'flex flex-col h-full',
|
|
424
|
+
body: 'flex-1 overflow-hidden sm:px-0 px-0 py-0 sm:py-0',
|
|
425
|
+
header: 'sm:px-4 px-4 py-4 sm:py-4',
|
|
426
|
+
}"
|
|
427
|
+
:data-testid="templateWhatsappCreateTestIds.translationsCard"
|
|
428
|
+
>
|
|
429
|
+
<div
|
|
430
|
+
class="grid grid-cols-[auto_1fr] divide-x divide-muted overflow-auto max-h-full h-full"
|
|
431
|
+
>
|
|
432
|
+
<div class="overflow-y-auto">
|
|
433
|
+
<LanguageList
|
|
434
|
+
v-model:messages="messages"
|
|
435
|
+
v-model:language="targetLanguage"
|
|
436
|
+
render-type="whatsapp_v1"
|
|
437
|
+
:message-group-id="draftId"
|
|
438
|
+
@add-language="onAddLanguage"
|
|
439
|
+
@remove-language="onRemoveLanguage"
|
|
440
|
+
:main-language-id="baseLanguage"
|
|
441
|
+
/>
|
|
442
|
+
</div>
|
|
443
|
+
<div class="overflow-y-auto" v-if="messages[baseLanguage] && messages[targetLanguage]">
|
|
444
|
+
<TranslationEditor
|
|
445
|
+
:base-message="messages[baseLanguage]"
|
|
446
|
+
v-model:target-message="messages[targetLanguage]"
|
|
447
|
+
:messages="messages"
|
|
448
|
+
:current-language="targetLanguage"
|
|
449
|
+
:base-language="baseLanguage"
|
|
450
|
+
:message-group-id="draftId"
|
|
451
|
+
@auto-translate-tracked="onAutoTranslateTracked"
|
|
452
|
+
@preview="onPreview"
|
|
453
|
+
@filters-changed="onFiltersChanged"
|
|
454
|
+
/>
|
|
455
|
+
</div>
|
|
456
|
+
</div>
|
|
457
|
+
</UCard>
|
|
458
|
+
</UForm>
|
|
459
|
+
|
|
460
|
+
<IncompleteTranslationsDialog
|
|
461
|
+
v-model="showIncompleteDialog"
|
|
462
|
+
:incomplete-languages="incompleteLanguages"
|
|
463
|
+
@save-without-translating="proceedWithSave"
|
|
464
|
+
@translate-and-save="translateAndSave"
|
|
465
|
+
@go-to-translations="goToTranslations"
|
|
466
|
+
/>
|
|
467
|
+
</SNavigationPage>
|
|
468
|
+
</template>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export default defineNuxtPlugin(() => {
|
|
2
|
+
const appConfig = useAppConfig()
|
|
3
|
+
if (!appConfig.composer?.tracking) return
|
|
4
|
+
|
|
5
|
+
const { trackPageView } = useTracking()
|
|
6
|
+
const router = useRouter()
|
|
7
|
+
|
|
8
|
+
router.afterEach((to) => {
|
|
9
|
+
const pageName = (to.name as string) || to.path
|
|
10
|
+
trackPageView(pageName)
|
|
11
|
+
})
|
|
12
|
+
})
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { useMutation } from '@pinia/colada'
|
|
2
|
+
import { aiApi } from '~/api'
|
|
3
|
+
|
|
4
|
+
export function useRefineTextMutation() {
|
|
5
|
+
return useMutation({
|
|
6
|
+
mutation: aiApi.refineText,
|
|
7
|
+
})
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function useTranslateMutation() {
|
|
11
|
+
return useMutation({
|
|
12
|
+
mutation: aiApi.translate,
|
|
13
|
+
})
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function useGenerateTextMutation() {
|
|
17
|
+
return useMutation({
|
|
18
|
+
mutation: aiApi.generateText,
|
|
19
|
+
})
|
|
20
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { useQuery, useMutation, useQueryCache } from '@pinia/colada'
|
|
2
|
+
import type {
|
|
3
|
+
BrandCreateBody,
|
|
4
|
+
BrandUpdateBody,
|
|
5
|
+
} from '@dev.smartpricing/message-composer-utils/types'
|
|
6
|
+
import { brandsApi, propertiesApi } from '~/api'
|
|
7
|
+
|
|
8
|
+
// QUERIES
|
|
9
|
+
export function useBrandsQuery(options?: {
|
|
10
|
+
sort_by?: 'created_at' | 'name'
|
|
11
|
+
direction?: 'asc' | 'desc'
|
|
12
|
+
}) {
|
|
13
|
+
return useQuery({
|
|
14
|
+
key: () => ['brands', options?.sort_by, options?.direction],
|
|
15
|
+
query: () => brandsApi.getAll(options),
|
|
16
|
+
staleTime: 2 * 60 * 1000,
|
|
17
|
+
})
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function useBrandQuery(id: MaybeRefOrGetter<string>) {
|
|
21
|
+
return useQuery({
|
|
22
|
+
key: () => ['brands', toValue(id)],
|
|
23
|
+
query: () => brandsApi.getById(toValue(id)),
|
|
24
|
+
staleTime: 2 * 60 * 1000,
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function usePropertiesQuery() {
|
|
29
|
+
return useQuery({
|
|
30
|
+
key: () => ['properties'],
|
|
31
|
+
query: () => propertiesApi.getAll(),
|
|
32
|
+
staleTime: 10 * 60 * 1000,
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// MUTATIONS
|
|
37
|
+
export function useCreateBrandMutation() {
|
|
38
|
+
const queryCache = useQueryCache()
|
|
39
|
+
|
|
40
|
+
return useMutation({
|
|
41
|
+
mutation: (data: BrandCreateBody) => brandsApi.create(data),
|
|
42
|
+
onSuccess: () => {
|
|
43
|
+
queryCache.invalidateQueries({ key: ['brands'] })
|
|
44
|
+
},
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function useUpdateBrandMutation() {
|
|
49
|
+
const queryCache = useQueryCache()
|
|
50
|
+
|
|
51
|
+
return useMutation({
|
|
52
|
+
mutation: ({ id, data }: { id: string; data: BrandUpdateBody }) => brandsApi.update(id, data),
|
|
53
|
+
onSuccess: (_, { id }) => {
|
|
54
|
+
queryCache.invalidateQueries({ key: ['brands', id] })
|
|
55
|
+
queryCache.invalidateQueries({ key: ['brands'] })
|
|
56
|
+
},
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function useDeleteBrandMutation() {
|
|
61
|
+
const queryCache = useQueryCache()
|
|
62
|
+
|
|
63
|
+
return useMutation({
|
|
64
|
+
mutation: brandsApi.delete,
|
|
65
|
+
onSuccess: () => {
|
|
66
|
+
queryCache.invalidateQueries({ key: ['brands'] })
|
|
67
|
+
},
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function useBrandPreviewMutation() {
|
|
72
|
+
return useMutation({
|
|
73
|
+
mutation: brandsApi.preview,
|
|
74
|
+
})
|
|
75
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useMutation, useQuery } from '@pinia/colada'
|
|
2
|
+
import type { MaybeRefOrGetter } from 'vue'
|
|
3
|
+
import { toValue } from 'vue'
|
|
4
|
+
import { compilationApi } from '~/api'
|
|
5
|
+
import type {
|
|
6
|
+
CompileMessageRequest,
|
|
7
|
+
PreviewMessageRequest,
|
|
8
|
+
} from '@dev.smartpricing/message-composer-utils/types'
|
|
9
|
+
|
|
10
|
+
export function useSendTestEmailMutation() {
|
|
11
|
+
return useMutation({
|
|
12
|
+
mutation: compilationApi.sendTestEmail,
|
|
13
|
+
})
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function useEmailParametersQuery(
|
|
17
|
+
messageGroupId: MaybeRefOrGetter<string>,
|
|
18
|
+
language: MaybeRefOrGetter<string>,
|
|
19
|
+
) {
|
|
20
|
+
return useQuery({
|
|
21
|
+
key: () => ['email-parameters', toValue(messageGroupId), toValue(language)],
|
|
22
|
+
query: () => compilationApi.getParameters(toValue(messageGroupId), toValue(language)),
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function useCompileEmailMutation() {
|
|
27
|
+
return useMutation({
|
|
28
|
+
mutation: ({ messageGroupId, data }: { messageGroupId: string; data: CompileMessageRequest }) =>
|
|
29
|
+
compilationApi.compileEmail(messageGroupId, data),
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function usePreviewEmailMutation() {
|
|
34
|
+
return useMutation({
|
|
35
|
+
mutation: (data: PreviewMessageRequest) => compilationApi.previewEmail(data),
|
|
36
|
+
})
|
|
37
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { useQuery } from '@pinia/colada'
|
|
2
|
+
import { dynamicValuesApi } from '~/api'
|
|
3
|
+
import { useAppContextStore } from '~/stores/appContext'
|
|
4
|
+
|
|
5
|
+
export function useDynamicValuesQuery() {
|
|
6
|
+
const { product, mainLanguage } = storeToRefs(useAppContextStore())
|
|
7
|
+
|
|
8
|
+
return useQuery({
|
|
9
|
+
key: () => ['dynamic-values', product.value, mainLanguage.value],
|
|
10
|
+
query: () => dynamicValuesApi.get(product.value || 'browser', mainLanguage.value || 'en'),
|
|
11
|
+
staleTime: 30 * 60 * 1000,
|
|
12
|
+
})
|
|
13
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { useMutation, useQuery } from '@pinia/colada'
|
|
2
|
+
import { mediaApi } from '~/api'
|
|
3
|
+
|
|
4
|
+
export function useMediaQuery() {
|
|
5
|
+
return useQuery({
|
|
6
|
+
key: ['media'],
|
|
7
|
+
query: mediaApi.list,
|
|
8
|
+
})
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function useUploadMediaMutation() {
|
|
12
|
+
return useMutation({
|
|
13
|
+
mutation: mediaApi.upload,
|
|
14
|
+
})
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function useUploadToSmartchatMutation() {
|
|
18
|
+
return useMutation({
|
|
19
|
+
mutation: mediaApi.uploadToSmartchat,
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function useDeleteMediaMutation() {
|
|
24
|
+
return useMutation({
|
|
25
|
+
mutation: mediaApi.delete,
|
|
26
|
+
})
|
|
27
|
+
}
|