@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,561 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
definePageMeta({
|
|
3
|
+
name: 'whatsapp-message-edit',
|
|
4
|
+
middleware: ['channel-guard'],
|
|
5
|
+
})
|
|
6
|
+
|
|
7
|
+
import WhatsappEditorSyncWrapper from '@/components/Whatsapp/Editor/SyncWrapper.vue'
|
|
8
|
+
import WhatsappPreview from '@/components/Whatsapp/PreviewPanel.vue'
|
|
9
|
+
import TranslationEditor from '@/components/Whatsapp/TranslationEditor.vue'
|
|
10
|
+
import LanguageList from '@/components/Common/LanguageList.vue'
|
|
11
|
+
import IncompleteTranslationsDialog from '@/components/Common/IncompleteTranslationsDialog.vue'
|
|
12
|
+
|
|
13
|
+
import CategoryChanger from '@/components/Whatsapp/CategoryChanger.vue'
|
|
14
|
+
import { computed, ref } from 'vue'
|
|
15
|
+
import {
|
|
16
|
+
type MessageFromDb,
|
|
17
|
+
type MessageGroupWithMessages,
|
|
18
|
+
type MessageGroupCategory,
|
|
19
|
+
type WhatsappMessage,
|
|
20
|
+
type LanguageFromDb,
|
|
21
|
+
type PostEntity,
|
|
22
|
+
} from '@dev.smartpricing/message-composer-utils/types'
|
|
23
|
+
import { useRoute } from 'vue-router'
|
|
24
|
+
import { useMessageGroupQuery, useLanguagesQuery } from '~/queries'
|
|
25
|
+
import { useMetaSubmissionStore } from '@/stores/metaSubmission'
|
|
26
|
+
import { useAppContextStore } from '@/stores/appContext'
|
|
27
|
+
import type { WaHeaderType } from '~/types/tracking'
|
|
28
|
+
|
|
29
|
+
const { trackEvent } = useTracking()
|
|
30
|
+
const route = useRoute()
|
|
31
|
+
|
|
32
|
+
const { mainLanguage } = storeToRefs(useAppContextStore())
|
|
33
|
+
|
|
34
|
+
const language = ref(mainLanguage.value)
|
|
35
|
+
const { goBackOrFallback } = useGoBack()
|
|
36
|
+
|
|
37
|
+
const activeTab = ref<'content' | 'translations'>('content')
|
|
38
|
+
const targetLanguage = ref(mainLanguage.value)
|
|
39
|
+
|
|
40
|
+
const messages = ref<Record<string, WhatsappMessage>>({})
|
|
41
|
+
const { data: messageGroupData, isPending: isFetching } = useMessageGroupQuery(
|
|
42
|
+
route.params.id as string,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
// Create a local editable copy to avoid mutating cache
|
|
46
|
+
const messageGroup = ref<MessageGroupWithMessages | null>(null)
|
|
47
|
+
|
|
48
|
+
// Initialize change tracking with messageGroupId
|
|
49
|
+
const messageGroupId = computed(() => route.params.id as string)
|
|
50
|
+
const changeTracking = useChangeTracking(messageGroupId)
|
|
51
|
+
|
|
52
|
+
// Store original messageGroup values
|
|
53
|
+
const originalMessageGroupName = ref<string>('')
|
|
54
|
+
const originalMessageGroupCategory = ref<MessageGroupCategory>('marketing')
|
|
55
|
+
|
|
56
|
+
// Watch for data changes to process messages
|
|
57
|
+
watch(
|
|
58
|
+
messageGroupData,
|
|
59
|
+
(newValue) => {
|
|
60
|
+
if (!newValue?.messages) return
|
|
61
|
+
|
|
62
|
+
// Deep clone to avoid mutating cache
|
|
63
|
+
messageGroup.value = structuredClone(newValue)
|
|
64
|
+
|
|
65
|
+
// Store original messageGroup metadata
|
|
66
|
+
originalMessageGroupName.value = newValue.name
|
|
67
|
+
originalMessageGroupCategory.value = newValue.category
|
|
68
|
+
|
|
69
|
+
for (const message of newValue.messages) {
|
|
70
|
+
if (message.render_type === 'whatsapp_v1') {
|
|
71
|
+
// Clone message to avoid cache mutation
|
|
72
|
+
messages.value[message.language_id] = structuredClone(message) as WhatsappMessage
|
|
73
|
+
// Record original state for change tracking
|
|
74
|
+
changeTracking.recordOriginalState(message.language_id, message as WhatsappMessage)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const resolved = resolveDefaultLanguage(messages.value, mainLanguage.value)
|
|
78
|
+
if (resolved && resolved !== mainLanguage.value) {
|
|
79
|
+
language.value = resolved
|
|
80
|
+
targetLanguage.value = resolved
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
{ immediate: true },
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
const { formState, draftSchema, validationErrors, validateDraft, validateFull, clearErrors } =
|
|
87
|
+
useTemplateFormValidation({
|
|
88
|
+
type: 'whatsapp',
|
|
89
|
+
name: computed(() => messageGroup.value?.name ?? ''),
|
|
90
|
+
category: computed(() => messageGroup.value?.category ?? 'marketing'),
|
|
91
|
+
messages,
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
const { saving, createTemplate, updateTemplate } = useTemplateOperations({
|
|
95
|
+
onSuccess: () => {
|
|
96
|
+
saved.value = true
|
|
97
|
+
changeTracking.clearAllChanges()
|
|
98
|
+
},
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
const { submitToMeta } = useMetaSubmissionStore()
|
|
102
|
+
|
|
103
|
+
// Create computed property for category to watch changes
|
|
104
|
+
const currentCategory = computed(() => messageGroup.value?.category)
|
|
105
|
+
|
|
106
|
+
// Initialize WhatsApp template behaviors composable
|
|
107
|
+
const { ensureUnsubscribeButton } = useWhatsappTemplateBehaviors({
|
|
108
|
+
category: currentCategory,
|
|
109
|
+
messages,
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
const saved = ref(false)
|
|
113
|
+
|
|
114
|
+
const hasAnyChanges = computed(() => {
|
|
115
|
+
// Check if message group name or category changed
|
|
116
|
+
if (messageGroup.value) {
|
|
117
|
+
const nameChanged = messageGroup.value.name !== originalMessageGroupName.value
|
|
118
|
+
const categoryChanged = messageGroup.value.category !== originalMessageGroupCategory.value
|
|
119
|
+
if (nameChanged || categoryChanged) return true
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Check if any language has changes
|
|
123
|
+
for (const languageId in messages.value) {
|
|
124
|
+
const message = messages.value[languageId]
|
|
125
|
+
if (message && changeTracking.hasChanges(languageId, message as WhatsappMessage)) {
|
|
126
|
+
return true
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return false
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
useExitConfirmation(computed(() => hasAnyChanges.value && !saved.value))
|
|
133
|
+
|
|
134
|
+
// ── Tracking helpers ──
|
|
135
|
+
function getWaBaseProps() {
|
|
136
|
+
return { name: messageGroup.value?.name ?? '', category: messageGroup.value?.category ?? '' }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function getWaDraftProps() {
|
|
140
|
+
const resolved = resolveDefaultLanguage(messages.value, mainLanguage.value)
|
|
141
|
+
const msg = resolved ? messages.value[resolved] : undefined
|
|
142
|
+
return {
|
|
143
|
+
...getWaBaseProps(),
|
|
144
|
+
languages: Object.keys(messages.value),
|
|
145
|
+
header: (msg?.body?.header?.type ?? 'none') as WaHeaderType,
|
|
146
|
+
footer: !!msg?.body?.footer?.enabled,
|
|
147
|
+
buttons: msg?.body?.buttons?.buttons?.length ?? 0,
|
|
148
|
+
is_internal: messageGroup.value?.visibility === 'internal',
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
watch(activeTab, (tab) => {
|
|
153
|
+
trackEvent('wa_edit.change_tab', { ...getWaBaseProps(), tab })
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
function onAddLanguage(lang: string) {
|
|
157
|
+
trackEvent('wa_edit.add_language', { ...getWaBaseProps(), language: lang })
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function onRemoveLanguage(lang: string) {
|
|
161
|
+
trackEvent('wa_edit.remove_language', {
|
|
162
|
+
...getWaBaseProps(),
|
|
163
|
+
language: lang,
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function onAutoTranslateTracked(lang: string) {
|
|
168
|
+
trackEvent('wa_edit.auto_translate', {
|
|
169
|
+
...getWaBaseProps(),
|
|
170
|
+
language: lang,
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function onPreview(lang: string) {
|
|
175
|
+
trackEvent('wa_edit.preview', { ...getWaBaseProps(), language: lang })
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function onFiltersChanged(payload: {
|
|
179
|
+
language: string
|
|
180
|
+
search_term: string
|
|
181
|
+
show_only_missing: boolean
|
|
182
|
+
show_only_changes: boolean
|
|
183
|
+
}) {
|
|
184
|
+
trackEvent('wa_edit.translations_filters', { ...getWaBaseProps(), ...payload })
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Get languages for incomplete translations check
|
|
188
|
+
const { data: languages } = useLanguagesQuery()
|
|
189
|
+
|
|
190
|
+
// Check for incomplete translations
|
|
191
|
+
const { incompleteLanguages, hasIncompleteTranslations } = useIncompleteTranslations({
|
|
192
|
+
messages,
|
|
193
|
+
languages,
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
// Dialog state and pending action
|
|
197
|
+
const showIncompleteDialog = ref(false)
|
|
198
|
+
const pendingAction = ref<'save_draft' | 'save_as_new' | 'request_approval' | null>(null)
|
|
199
|
+
|
|
200
|
+
// Auto-translate composable
|
|
201
|
+
const { translateMessage, isTranslating } = useAutoTranslate()
|
|
202
|
+
|
|
203
|
+
async function editTemplate() {
|
|
204
|
+
if (!messageGroup.value) return
|
|
205
|
+
|
|
206
|
+
const templateData = {
|
|
207
|
+
messageGroup: {
|
|
208
|
+
name: messageGroup.value.name,
|
|
209
|
+
category: messageGroup.value.category,
|
|
210
|
+
},
|
|
211
|
+
messages: messages.value,
|
|
212
|
+
}
|
|
213
|
+
await updateTemplate(route.params.id as string, templateData)
|
|
214
|
+
notifyParent('TEMPLATE_SAVED_DRAFT', { id: messageGroup.value.id, name: messageGroup.value.name })
|
|
215
|
+
goBackOrFallback()
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function saveAsNew() {
|
|
219
|
+
if (!messageGroup.value) return
|
|
220
|
+
trackEvent('wa_edit.save_as_new', getWaDraftProps())
|
|
221
|
+
|
|
222
|
+
const cleanedMessages: Record<string, PostEntity<WhatsappMessage>> = {}
|
|
223
|
+
for (const [languageId, message] of Object.entries(messages.value)) {
|
|
224
|
+
const { id, message_group_id, created_at, updated_at, ...templateMessage } = message
|
|
225
|
+
cleanedMessages[languageId] = {
|
|
226
|
+
...templateMessage,
|
|
227
|
+
message_group_id: '',
|
|
228
|
+
metadata: {},
|
|
229
|
+
} as PostEntity<WhatsappMessage>
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const templateData = {
|
|
233
|
+
messageGroup: {
|
|
234
|
+
name: messageGroup.value.name,
|
|
235
|
+
category: messageGroup.value.category,
|
|
236
|
+
},
|
|
237
|
+
messages: cleanedMessages,
|
|
238
|
+
}
|
|
239
|
+
const result = await createTemplate(templateData, 'draft')
|
|
240
|
+
if (result) {
|
|
241
|
+
notifyParent('TEMPLATE_SAVED_AS_NEW', { id: result.id, name: result.name })
|
|
242
|
+
navigateTo(`/template/whatsapp/${result.id}`, { replace: true })
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function requestApproval() {
|
|
247
|
+
if (!messageGroup.value) return
|
|
248
|
+
trackEvent('wa_edit.request_approval', getWaDraftProps())
|
|
249
|
+
|
|
250
|
+
const templateData = {
|
|
251
|
+
messageGroup: {
|
|
252
|
+
name: messageGroup.value.name,
|
|
253
|
+
category: messageGroup.value.category,
|
|
254
|
+
},
|
|
255
|
+
messages: messages.value,
|
|
256
|
+
}
|
|
257
|
+
await updateTemplate(route.params.id as string, templateData)
|
|
258
|
+
await submitToMeta(route.params.id as string)
|
|
259
|
+
notifyParent('TEMPLATE_APPROVAL_REQUESTED', {
|
|
260
|
+
id: messageGroup.value.id,
|
|
261
|
+
name: messageGroup.value.name,
|
|
262
|
+
})
|
|
263
|
+
goBackOrFallback()
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Wrapper functions to validate then check for incomplete translations
|
|
267
|
+
async function handleSaveDraft() {
|
|
268
|
+
if (!(await validateDraft())) return
|
|
269
|
+
if (hasIncompleteTranslations.value) {
|
|
270
|
+
pendingAction.value = 'save_draft'
|
|
271
|
+
showIncompleteDialog.value = true
|
|
272
|
+
} else {
|
|
273
|
+
editTemplate()
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function handleSaveAsNew() {
|
|
278
|
+
if (!(await validateDraft())) return
|
|
279
|
+
if (hasIncompleteTranslations.value) {
|
|
280
|
+
pendingAction.value = 'save_as_new'
|
|
281
|
+
showIncompleteDialog.value = true
|
|
282
|
+
} else {
|
|
283
|
+
saveAsNew()
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async function handleRequestApproval() {
|
|
288
|
+
if (!(await validateFull())) return
|
|
289
|
+
if (hasIncompleteTranslations.value) {
|
|
290
|
+
pendingAction.value = 'request_approval'
|
|
291
|
+
showIncompleteDialog.value = true
|
|
292
|
+
} else {
|
|
293
|
+
requestApproval()
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Proceed with save without translating
|
|
298
|
+
function proceedWithSave() {
|
|
299
|
+
if (pendingAction.value === 'save_draft') {
|
|
300
|
+
editTemplate()
|
|
301
|
+
} else if (pendingAction.value === 'save_as_new') {
|
|
302
|
+
saveAsNew()
|
|
303
|
+
} else if (pendingAction.value === 'request_approval') {
|
|
304
|
+
requestApproval()
|
|
305
|
+
}
|
|
306
|
+
pendingAction.value = null
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Go to translations tab
|
|
310
|
+
function goToTranslations() {
|
|
311
|
+
activeTab.value = 'translations'
|
|
312
|
+
pendingAction.value = null
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Translate automatically and save
|
|
316
|
+
async function translateAndSave() {
|
|
317
|
+
if (!incompleteLanguages.value.length) {
|
|
318
|
+
proceedWithSave()
|
|
319
|
+
return
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const baseMessage = messages.value[language.value]
|
|
323
|
+
if (!baseMessage) {
|
|
324
|
+
proceedWithSave()
|
|
325
|
+
return
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Translate all incomplete languages
|
|
329
|
+
for (const { language: lang } of incompleteLanguages.value) {
|
|
330
|
+
const targetMessage = messages.value[lang.id]
|
|
331
|
+
if (!targetMessage) continue
|
|
332
|
+
|
|
333
|
+
await translateMessage(
|
|
334
|
+
baseMessage as WhatsappMessage,
|
|
335
|
+
targetMessage as WhatsappMessage,
|
|
336
|
+
language.value,
|
|
337
|
+
lang.id,
|
|
338
|
+
)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// After translation, proceed with save
|
|
342
|
+
proceedWithSave()
|
|
343
|
+
}
|
|
344
|
+
const isPending = computed(() =>
|
|
345
|
+
messageGroup.value?.messages?.some((message: MessageFromDb) => message.status === 'pending'),
|
|
346
|
+
)
|
|
347
|
+
const isApproved = computed(() =>
|
|
348
|
+
messageGroup.value?.messages?.some((message: MessageFromDb) => message.status === 'approved'),
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
watch(
|
|
352
|
+
() => messages.value[mainLanguage.value],
|
|
353
|
+
() => {
|
|
354
|
+
const resolved = resolveDefaultLanguage(messages.value, mainLanguage.value)
|
|
355
|
+
if (resolved && resolved !== mainLanguage.value) {
|
|
356
|
+
language.value = resolved
|
|
357
|
+
targetLanguage.value = resolved
|
|
358
|
+
}
|
|
359
|
+
},
|
|
360
|
+
)
|
|
361
|
+
</script>
|
|
362
|
+
|
|
363
|
+
<template>
|
|
364
|
+
<SNavigationPage>
|
|
365
|
+
<template #header>
|
|
366
|
+
<SNavigationBarHeader
|
|
367
|
+
v-if="messageGroup"
|
|
368
|
+
:title="$t('pages.whatsapp.edit_message_group.title')"
|
|
369
|
+
:tabs="[
|
|
370
|
+
{ label: $t('pages.whatsapp.tabs.content'), value: 'content' },
|
|
371
|
+
{ label: $t('pages.whatsapp.tabs.translations'), value: 'translations' },
|
|
372
|
+
]"
|
|
373
|
+
:active-tab="activeTab"
|
|
374
|
+
:data-testid="templateWhatsappEditTestIds.navbar"
|
|
375
|
+
@tab-change="activeTab = $event"
|
|
376
|
+
>
|
|
377
|
+
<template #actions>
|
|
378
|
+
<UButton
|
|
379
|
+
@click="
|
|
380
|
+
() => {
|
|
381
|
+
notifyParent('TEMPLATE_CANCELLED', { id: messageGroup?.id })
|
|
382
|
+
goBackOrFallback()
|
|
383
|
+
}
|
|
384
|
+
"
|
|
385
|
+
:label="$t('common.actions.cancel')"
|
|
386
|
+
color="primary"
|
|
387
|
+
variant="ghost"
|
|
388
|
+
:loading="saving || isTranslating"
|
|
389
|
+
:data-testid="templateWhatsappEditTestIds.cancelButton"
|
|
390
|
+
/>
|
|
391
|
+
<UButton
|
|
392
|
+
v-if="isPending || isApproved"
|
|
393
|
+
@click="handleSaveAsNew()"
|
|
394
|
+
:label="$t('pages.whatsapp.create_message_group.save_as_new')"
|
|
395
|
+
color="secondary"
|
|
396
|
+
variant="outline"
|
|
397
|
+
:loading="saving || isTranslating"
|
|
398
|
+
:data-testid="templateWhatsappEditTestIds.saveAsNewButton"
|
|
399
|
+
/>
|
|
400
|
+
<UButton
|
|
401
|
+
v-else
|
|
402
|
+
@click="handleSaveDraft()"
|
|
403
|
+
:label="$t('pages.whatsapp.create_message_group.save_draft')"
|
|
404
|
+
color="secondary"
|
|
405
|
+
variant="outline"
|
|
406
|
+
:loading="saving || isTranslating"
|
|
407
|
+
:data-testid="templateWhatsappEditTestIds.saveDraftButton"
|
|
408
|
+
/>
|
|
409
|
+
<UButton
|
|
410
|
+
v-if="!isPending && !isApproved"
|
|
411
|
+
@click="handleRequestApproval()"
|
|
412
|
+
:label="$t('pages.whatsapp.create_message_group.request_approval')"
|
|
413
|
+
color="primary"
|
|
414
|
+
:loading="saving || isTranslating"
|
|
415
|
+
:data-testid="templateWhatsappEditTestIds.requestApprovalButton"
|
|
416
|
+
/>
|
|
417
|
+
</template>
|
|
418
|
+
</SNavigationBarHeader>
|
|
419
|
+
</template>
|
|
420
|
+
|
|
421
|
+
<div v-if="validationErrors.length">
|
|
422
|
+
<UAlert
|
|
423
|
+
color="error"
|
|
424
|
+
variant="subtle"
|
|
425
|
+
icon="i-lucide-alert-triangle"
|
|
426
|
+
:title="$t('editor.validation.error.title')"
|
|
427
|
+
:close="true"
|
|
428
|
+
@update:open="clearErrors()"
|
|
429
|
+
:data-testid="templateWhatsappEditTestIds.validationAlert"
|
|
430
|
+
>
|
|
431
|
+
<template #description>
|
|
432
|
+
<ul class="list-disc pl-4 space-y-1">
|
|
433
|
+
<li v-for="err in validationErrors" :key="err">
|
|
434
|
+
{{ err }}
|
|
435
|
+
</li>
|
|
436
|
+
</ul>
|
|
437
|
+
</template>
|
|
438
|
+
</UAlert>
|
|
439
|
+
</div>
|
|
440
|
+
|
|
441
|
+
<UForm
|
|
442
|
+
ref="templateForm"
|
|
443
|
+
:schema="draftSchema"
|
|
444
|
+
:state="formState"
|
|
445
|
+
:validate-on="[]"
|
|
446
|
+
class="flex-1 relative min-h-0 flex flex-col"
|
|
447
|
+
>
|
|
448
|
+
<UCard
|
|
449
|
+
v-if="activeTab === 'content' && messageGroup"
|
|
450
|
+
:ui="{
|
|
451
|
+
root: 'flex flex-col',
|
|
452
|
+
body: 'flex-1 overflow-hidden sm:px-0 px-0 py-0 sm:py-0',
|
|
453
|
+
header: 'sm:px-4 px-4 py-4 sm:py-4',
|
|
454
|
+
}"
|
|
455
|
+
:data-testid="templateWhatsappEditTestIds.contentCard"
|
|
456
|
+
>
|
|
457
|
+
<template #header>
|
|
458
|
+
<div class="grid grid-cols-3 gap-2">
|
|
459
|
+
<UFieldGroup>
|
|
460
|
+
<UBadge variant="outline" color="neutral" size="lg">{{
|
|
461
|
+
$t('pages.whatsapp.edit_message_group.name')
|
|
462
|
+
}}</UBadge>
|
|
463
|
+
<UInput
|
|
464
|
+
v-model="messageGroup.name"
|
|
465
|
+
:placeholder="$t('pages.whatsapp.edit_message_group.name_placeholder')"
|
|
466
|
+
:data-testid="templateWhatsappEditTestIds.nameInput"
|
|
467
|
+
/>
|
|
468
|
+
</UFieldGroup>
|
|
469
|
+
|
|
470
|
+
<UFieldGroup>
|
|
471
|
+
<UBadge variant="outline" color="neutral" size="lg">{{
|
|
472
|
+
$t('pages.whatsapp.edit_message_group.category')
|
|
473
|
+
}}</UBadge>
|
|
474
|
+
<CategoryChanger
|
|
475
|
+
v-model:category="messageGroup.category"
|
|
476
|
+
@changed="ensureUnsubscribeButton"
|
|
477
|
+
/>
|
|
478
|
+
</UFieldGroup>
|
|
479
|
+
<!-- <DocumentationPanel
|
|
480
|
+
:category="$t('documentation.whatsapp_category.category')"
|
|
481
|
+
:title="$t('documentation.whatsapp_category.title')"
|
|
482
|
+
:description="$t('documentation.whatsapp_category.description')"
|
|
483
|
+
:info="{
|
|
484
|
+
title: '',
|
|
485
|
+
text: $t('documentation.whatsapp_category.info'),
|
|
486
|
+
}"
|
|
487
|
+
:content="[
|
|
488
|
+
$t('documentation.whatsapp_category.content.marketing'),
|
|
489
|
+
$t('documentation.whatsapp_category.content.utility'),
|
|
490
|
+
]"
|
|
491
|
+
/> -->
|
|
492
|
+
</div>
|
|
493
|
+
</template>
|
|
494
|
+
|
|
495
|
+
<div class="grid grid-cols-3 divide-x divide-muted overflow-auto max-h-full h-full">
|
|
496
|
+
<div class="col-span-2 overflow-y-auto pt-4 pb-4" v-if="messages[language]">
|
|
497
|
+
<h2 class="px-4 pb-4 label-lg">
|
|
498
|
+
{{ $t('pages.whatsapp.edit_message_group.message_content') }}
|
|
499
|
+
</h2>
|
|
500
|
+
<WhatsappEditorSyncWrapper
|
|
501
|
+
v-model="messages[language]"
|
|
502
|
+
:messages="messages"
|
|
503
|
+
:main-language-id="language"
|
|
504
|
+
:key="language"
|
|
505
|
+
/>
|
|
506
|
+
</div>
|
|
507
|
+
<div class="col-span-1 overflow-y-auto" v-if="messages[language]">
|
|
508
|
+
<WhatsappPreview :message="messages[language]" />
|
|
509
|
+
</div>
|
|
510
|
+
</div>
|
|
511
|
+
</UCard>
|
|
512
|
+
|
|
513
|
+
<UCard
|
|
514
|
+
v-if="activeTab === 'translations' && messageGroup"
|
|
515
|
+
:ui="{
|
|
516
|
+
root: 'flex flex-col h-full',
|
|
517
|
+
body: 'flex-1 overflow-hidden sm:px-0 px-0 py-0 sm:py-0',
|
|
518
|
+
header: 'sm:px-4 px-4 py-4 sm:py-4',
|
|
519
|
+
}"
|
|
520
|
+
:data-testid="templateWhatsappEditTestIds.translationsCard"
|
|
521
|
+
>
|
|
522
|
+
<div
|
|
523
|
+
class="grid grid-cols-[auto_1fr] divide-x divide-muted overflow-auto max-h-full h-full"
|
|
524
|
+
>
|
|
525
|
+
<div class="overflow-y-auto">
|
|
526
|
+
<LanguageList
|
|
527
|
+
v-model:messages="messages"
|
|
528
|
+
v-model:language="targetLanguage"
|
|
529
|
+
render-type="whatsapp_v1"
|
|
530
|
+
:message-group-id="messageGroupId"
|
|
531
|
+
:main-language-id="language"
|
|
532
|
+
@add-language="onAddLanguage"
|
|
533
|
+
@remove-language="onRemoveLanguage"
|
|
534
|
+
/>
|
|
535
|
+
</div>
|
|
536
|
+
<div class="overflow-y-auto" v-if="messages[language] && messages[targetLanguage]">
|
|
537
|
+
<TranslationEditor
|
|
538
|
+
:base-message="messages[language] || messages['en']"
|
|
539
|
+
v-model:target-message="messages[targetLanguage]"
|
|
540
|
+
:messages="messages"
|
|
541
|
+
:current-language="targetLanguage"
|
|
542
|
+
:base-language="language"
|
|
543
|
+
:message-group-id="messageGroupId"
|
|
544
|
+
@auto-translate-tracked="onAutoTranslateTracked"
|
|
545
|
+
@preview="onPreview"
|
|
546
|
+
@filters-changed="onFiltersChanged"
|
|
547
|
+
/>
|
|
548
|
+
</div>
|
|
549
|
+
</div>
|
|
550
|
+
</UCard>
|
|
551
|
+
</UForm>
|
|
552
|
+
|
|
553
|
+
<IncompleteTranslationsDialog
|
|
554
|
+
v-model="showIncompleteDialog"
|
|
555
|
+
:incomplete-languages="incompleteLanguages"
|
|
556
|
+
@save-without-translating="proceedWithSave"
|
|
557
|
+
@translate-and-save="translateAndSave"
|
|
558
|
+
@go-to-translations="goToTranslations"
|
|
559
|
+
/>
|
|
560
|
+
</SNavigationPage>
|
|
561
|
+
</template>
|