@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.
Files changed (169) hide show
  1. package/README.md +75 -0
  2. package/app/api/ai.ts +28 -0
  3. package/app/api/brands.ts +54 -0
  4. package/app/api/client.ts +56 -0
  5. package/app/api/compilation.ts +34 -0
  6. package/app/api/dynamicValues.ts +11 -0
  7. package/app/api/index.ts +11 -0
  8. package/app/api/languages.ts +6 -0
  9. package/app/api/media.ts +26 -0
  10. package/app/api/messageGroups.ts +49 -0
  11. package/app/api/messages.ts +22 -0
  12. package/app/api/meta.ts +18 -0
  13. package/app/api/tracking.ts +9 -0
  14. package/app/components/Brand/BrandDrawer.vue +281 -0
  15. package/app/components/Brand/BrandPreview.vue +137 -0
  16. package/app/components/Brand/Steps/BrandStepGeneral.vue +37 -0
  17. package/app/components/Brand/Steps/BrandStepSocials.vue +48 -0
  18. package/app/components/Brand/Steps/BrandStepVisual.vue +214 -0
  19. package/app/components/Common/BrandColorSwatches.vue +37 -0
  20. package/app/components/Common/ColorInput.vue +144 -0
  21. package/app/components/Common/EmojiPickerButton.vue +35 -0
  22. package/app/components/Common/FontFamilySelect.vue +39 -0
  23. package/app/components/Common/HtmlPreviewIframe.vue +68 -0
  24. package/app/components/Common/IncompleteTranslationsDialog.vue +97 -0
  25. package/app/components/Common/LanguageList.vue +356 -0
  26. package/app/components/Common/LoadingOverlay.vue +12 -0
  27. package/app/components/Common/MediaUploadOrSelect.vue +299 -0
  28. package/app/components/Common/MessagePreviewSlideover.vue +88 -0
  29. package/app/components/Common/MonacoEditor.vue +110 -0
  30. package/app/components/Common/PhoneInput.vue +804 -0
  31. package/app/components/Common/SectionHeader.vue +26 -0
  32. package/app/components/Common/TranslationActionBar.vue +139 -0
  33. package/app/components/Common/ViewLessMore.vue +40 -0
  34. package/app/components/CreateTemplateButton.vue +59 -0
  35. package/app/components/DynamicDataButton.vue +75 -0
  36. package/app/components/DynamicDataSlideover.vue +149 -0
  37. package/app/components/Editor/AiGenerateSlideover.vue +292 -0
  38. package/app/components/Editor/EditorLinkPopover.vue +154 -0
  39. package/app/components/Editor/RichTextEditor.vue +495 -0
  40. package/app/components/Email/Editor/BlockLibrary.vue +89 -0
  41. package/app/components/Email/Editor/BlockSettings.vue +60 -0
  42. package/app/components/Email/Editor/Blocks/ButtonBlock.vue +117 -0
  43. package/app/components/Email/Editor/Blocks/ButtonSettings.vue +228 -0
  44. package/app/components/Email/Editor/Blocks/DiscountCodeBlock.vue +74 -0
  45. package/app/components/Email/Editor/Blocks/DiscountCodeSettings.vue +162 -0
  46. package/app/components/Email/Editor/Blocks/DividerBlock.vue +31 -0
  47. package/app/components/Email/Editor/Blocks/DividerSettings.vue +108 -0
  48. package/app/components/Email/Editor/Blocks/FooterBlock.vue +61 -0
  49. package/app/components/Email/Editor/Blocks/FooterSettings.vue +123 -0
  50. package/app/components/Email/Editor/Blocks/GridBlock.vue +128 -0
  51. package/app/components/Email/Editor/Blocks/GridSettings.vue +109 -0
  52. package/app/components/Email/Editor/Blocks/HTMLBlock.vue +31 -0
  53. package/app/components/Email/Editor/Blocks/HTMLSettings.vue +85 -0
  54. package/app/components/Email/Editor/Blocks/HeaderBlock.vue +53 -0
  55. package/app/components/Email/Editor/Blocks/HeaderSettings.vue +113 -0
  56. package/app/components/Email/Editor/Blocks/HeadingBlock.vue +61 -0
  57. package/app/components/Email/Editor/Blocks/HeadingSettings.vue +156 -0
  58. package/app/components/Email/Editor/Blocks/ImageBlock.vue +49 -0
  59. package/app/components/Email/Editor/Blocks/ImageSettings.vue +53 -0
  60. package/app/components/Email/Editor/Blocks/IndexBlock.vue +155 -0
  61. package/app/components/Email/Editor/Blocks/ParagraphBlock.vue +61 -0
  62. package/app/components/Email/Editor/Blocks/ParagraphSettings.vue +157 -0
  63. package/app/components/Email/Editor/Blocks/SocialsBlock.vue +63 -0
  64. package/app/components/Email/Editor/Blocks/SocialsSettings.vue +117 -0
  65. package/app/components/Email/Editor/Blocks/SpacerBlock.vue +37 -0
  66. package/app/components/Email/Editor/Blocks/SpacerSettings.vue +95 -0
  67. package/app/components/Email/Editor/Blocks/TopProductsBlock.vue +47 -0
  68. package/app/components/Email/Editor/Blocks/TopProductsSettings.vue +87 -0
  69. package/app/components/Email/Editor/Blocks/UnsubscribeLinkBlock.vue +61 -0
  70. package/app/components/Email/Editor/Blocks/UnsubscribeLinkSettings.vue +123 -0
  71. package/app/components/Email/Editor/Canvas.vue +143 -0
  72. package/app/components/Email/Editor/GeneralStyleSettings.vue +125 -0
  73. package/app/components/Email/Editor/Index.vue +77 -0
  74. package/app/components/Email/Editor/LeftSidebar.vue +95 -0
  75. package/app/components/Email/InlinePreview.vue +124 -0
  76. package/app/components/Email/Preview.vue +126 -0
  77. package/app/components/Email/PreviewPanel.vue +57 -0
  78. package/app/components/Email/SendTestEmailModal.vue +121 -0
  79. package/app/components/Email/TranslationEditor.vue +198 -0
  80. package/app/components/Email/Translator/Button.vue +101 -0
  81. package/app/components/Email/Translator/Discount.vue +60 -0
  82. package/app/components/Email/Translator/Footer.vue +66 -0
  83. package/app/components/Email/Translator/Grid.vue +127 -0
  84. package/app/components/Email/Translator/Header.vue +90 -0
  85. package/app/components/Email/Translator/Heading.vue +66 -0
  86. package/app/components/Email/Translator/Image.vue +91 -0
  87. package/app/components/Email/Translator/Index.vue +138 -0
  88. package/app/components/Email/Translator/Paragraph.vue +66 -0
  89. package/app/components/Email/Translator/RawHtml.vue +64 -0
  90. package/app/components/Email/Translator/UnsubscribeLink.vue +59 -0
  91. package/app/components/TemplateList/BaseTable.vue +354 -0
  92. package/app/components/TemplateList/StatusCell.vue +67 -0
  93. package/app/components/Whatsapp/CategoryChanger.vue +63 -0
  94. package/app/components/Whatsapp/Editor/Body.vue +53 -0
  95. package/app/components/Whatsapp/Editor/Buttons.vue +341 -0
  96. package/app/components/Whatsapp/Editor/Footer.vue +70 -0
  97. package/app/components/Whatsapp/Editor/Header.vue +156 -0
  98. package/app/components/Whatsapp/Editor/Index.vue +89 -0
  99. package/app/components/Whatsapp/Editor/SyncWrapper.vue +166 -0
  100. package/app/components/Whatsapp/Preview.vue +197 -0
  101. package/app/components/Whatsapp/PreviewPanel.vue +24 -0
  102. package/app/components/Whatsapp/TranslationEditor.vue +150 -0
  103. package/app/components/Whatsapp/Translator/Body.vue +61 -0
  104. package/app/components/Whatsapp/Translator/Button.vue +209 -0
  105. package/app/components/Whatsapp/Translator/Footer.vue +56 -0
  106. package/app/components/Whatsapp/Translator/Header.vue +61 -0
  107. package/app/components/Whatsapp/Translator/HeaderMedia.vue +117 -0
  108. package/app/components/Whatsapp/Translator/Index.vue +159 -0
  109. package/app/composables/composerContext.ts +35 -0
  110. package/app/composables/useAutoTranslate.ts +242 -0
  111. package/app/composables/useAutoTranslateEmail.ts +294 -0
  112. package/app/composables/useChangeTracking.ts +212 -0
  113. package/app/composables/useChangeTrackingEmail.ts +227 -0
  114. package/app/composables/useChannels.ts +36 -0
  115. package/app/composables/useEmailBlockContext.ts +43 -0
  116. package/app/composables/useExitConfirmation.ts +39 -0
  117. package/app/composables/useGoBack.ts +15 -0
  118. package/app/composables/useIncompleteTranslations.ts +55 -0
  119. package/app/composables/useIncompleteTranslationsEmail.ts +56 -0
  120. package/app/composables/useMediaLimits.ts +42 -0
  121. package/app/composables/useTemplateFormValidation.ts +109 -0
  122. package/app/composables/useTemplateOperations.ts +147 -0
  123. package/app/composables/useTracking.ts +39 -0
  124. package/app/composables/useTranslationFilters.ts +176 -0
  125. package/app/composables/useTranslationFiltersEmail.ts +244 -0
  126. package/app/composables/useWhatsappTemplateBehaviors.ts +58 -0
  127. package/app/middleware/channel-guard.ts +13 -0
  128. package/app/pages/brands.vue +195 -0
  129. package/app/pages/images.vue +141 -0
  130. package/app/pages/index.vue +198 -0
  131. package/app/pages/preview/[id].vue +47 -0
  132. package/app/pages/template/email/[id].vue +596 -0
  133. package/app/pages/template/email/create.vue +562 -0
  134. package/app/pages/template/whatsapp/[id].vue +561 -0
  135. package/app/pages/template/whatsapp/create.vue +468 -0
  136. package/app/plugins/admin-shortcut.client.ts +7 -0
  137. package/app/plugins/tracking.client.ts +12 -0
  138. package/app/queries/ai.ts +20 -0
  139. package/app/queries/brands.ts +75 -0
  140. package/app/queries/compilation.ts +37 -0
  141. package/app/queries/dynamicValues.ts +13 -0
  142. package/app/queries/index.ts +9 -0
  143. package/app/queries/languages.ts +10 -0
  144. package/app/queries/media.ts +27 -0
  145. package/app/queries/messageGroups.ts +112 -0
  146. package/app/queries/messages.ts +42 -0
  147. package/app/queries/meta.ts +13 -0
  148. package/app/stores/adminStore.ts +50 -0
  149. package/app/stores/appContext.ts +99 -0
  150. package/app/stores/emailEditor.ts +220 -0
  151. package/app/stores/main.ts +3 -0
  152. package/app/stores/metaSubmission.ts +116 -0
  153. package/app/types/app-config.d.ts +14 -0
  154. package/app/types/hooks.d.ts +9 -0
  155. package/app/types/tracking.ts +148 -0
  156. package/app/utils/locale.ts +17 -0
  157. package/app/utils/notifyParent.ts +7 -0
  158. package/app/utils/resolveDefaultLanguage.ts +14 -0
  159. package/app/utils/socialDefaults.ts +42 -0
  160. package/app/utils/testIds.const.ts +728 -0
  161. package/app.config.ts +5 -0
  162. package/i18n/check-translations.ts +56 -0
  163. package/i18n/locales/de.ts +834 -0
  164. package/i18n/locales/en.ts +826 -0
  165. package/i18n/locales/es.ts +833 -0
  166. package/i18n/locales/fr.ts +837 -0
  167. package/i18n/locales/it.ts +831 -0
  168. package/nuxt.config.ts +65 -0
  169. package/package.json +64 -0
@@ -0,0 +1,68 @@
1
+ <script setup lang="ts">
2
+ import { useResizeObserver } from '@vueuse/core'
3
+
4
+ defineOptions({
5
+ name: 'HtmlPreviewIframe',
6
+ inheritAttrs: true,
7
+ })
8
+
9
+ const props = withDefaults(
10
+ defineProps<{
11
+ html: string
12
+ title: string
13
+ minHeight?: number
14
+ }>(),
15
+ { minHeight: 0 },
16
+ )
17
+
18
+ const iframeRef = useTemplateRef<HTMLIFrameElement>('iframe')
19
+ const measuredHeight = ref(0)
20
+
21
+ const heightStyle = computed(() => {
22
+ if (!measuredHeight.value) return `${props.minHeight}px`
23
+ return `${Math.max(measuredHeight.value + 2, props.minHeight)}px`
24
+ })
25
+
26
+ function fitToContent() {
27
+ const doc = iframeRef.value?.contentDocument
28
+ if (!doc) return
29
+ measuredHeight.value = doc.documentElement.scrollHeight || doc.body?.scrollHeight || 0
30
+ }
31
+
32
+ let stopObserver: (() => void) | undefined
33
+
34
+ function observeContent() {
35
+ stopObserver?.()
36
+ const doc = iframeRef.value?.contentDocument
37
+ if (!doc?.documentElement) return
38
+ const { stop } = useResizeObserver(doc.documentElement, () => fitToContent())
39
+ stopObserver = stop
40
+ }
41
+
42
+ function onLoad() {
43
+ fitToContent()
44
+ observeContent()
45
+ }
46
+
47
+ watch(
48
+ () => props.html,
49
+ () => {
50
+ measuredHeight.value = 0
51
+ },
52
+ )
53
+
54
+ onBeforeUnmount(() => {
55
+ stopObserver?.()
56
+ })
57
+ </script>
58
+
59
+ <template>
60
+ <iframe
61
+ ref="iframe"
62
+ :srcdoc="html"
63
+ :title="title"
64
+ sandbox="allow-same-origin"
65
+ :style="{ height: heightStyle }"
66
+ @load="onLoad"
67
+ />
68
+ </template>
@@ -0,0 +1,97 @@
1
+ <script setup lang="ts">
2
+ import type { LanguageFromDb } from '@dev.smartpricing/message-composer-utils/types'
3
+
4
+ interface Props {
5
+ incompleteLanguages: Array<{ language: LanguageFromDb; percentage: number }>
6
+ }
7
+
8
+ const props = defineProps<Props>()
9
+
10
+ const isOpen = defineModel<boolean>('modelValue', { required: true })
11
+
12
+ const emit = defineEmits<{
13
+ saveWithoutTranslating: []
14
+ translateAndSave: []
15
+ goToTranslations: []
16
+ }>()
17
+
18
+ const handleSaveWithoutTranslating = () => {
19
+ emit('saveWithoutTranslating')
20
+ isOpen.value = false
21
+ }
22
+
23
+ const handleTranslateAndSave = () => {
24
+ emit('translateAndSave')
25
+ isOpen.value = false
26
+ }
27
+
28
+ const handleGoToTranslations = () => {
29
+ emit('goToTranslations')
30
+ isOpen.value = false
31
+ }
32
+ </script>
33
+
34
+ <template>
35
+ <UModal
36
+ v-model:open="isOpen"
37
+ :title="$t('dialogs.incomplete_translations.title')"
38
+ close-icon="ph:x"
39
+ :close="{
40
+ onClick: handleGoToTranslations,
41
+ 'data-testid': incompleteTranslationsDialogTestIds.modalCloseButton,
42
+ }"
43
+ dismissible
44
+ :data-testid="incompleteTranslationsDialogTestIds.modal"
45
+ >
46
+ <template #body>
47
+ <div class="flex flex-col gap-4">
48
+ <!-- Warning message -->
49
+ <p
50
+ class="text-sm text-muted"
51
+ :data-testid="incompleteTranslationsDialogTestIds.description"
52
+ >
53
+ {{ $t('dialogs.incomplete_translations.description') }}
54
+ </p>
55
+
56
+ <!-- Info note -->
57
+ <UAlert
58
+ icon="ph:info"
59
+ variant="soft"
60
+ color="neutral"
61
+ :description="$t('dialogs.incomplete_translations.note')"
62
+ :data-testid="incompleteTranslationsDialogTestIds.noteAlert"
63
+ />
64
+ </div>
65
+ </template>
66
+
67
+ <template #footer>
68
+ <div class="flex gap-2 justify-between w-full">
69
+ <UButton
70
+ variant="link"
71
+ color="neutral"
72
+ @click="handleGoToTranslations"
73
+ :data-testid="incompleteTranslationsDialogTestIds.goToTranslationsButton"
74
+ >
75
+ {{ $t('dialogs.incomplete_translations.go_to_translations') }}
76
+ </UButton>
77
+ <div class="flex gap-2">
78
+ <UButton
79
+ variant="outline"
80
+ color="neutral"
81
+ @click="handleSaveWithoutTranslating"
82
+ :data-testid="incompleteTranslationsDialogTestIds.cancelButton"
83
+ >
84
+ {{ $t('common.actions.cancel') }}
85
+ </UButton>
86
+ <UButton
87
+ color="primary"
88
+ @click="handleTranslateAndSave"
89
+ :data-testid="incompleteTranslationsDialogTestIds.translateAndSaveButton"
90
+ >
91
+ {{ $t('dialogs.incomplete_translations.translate_and_save') }}
92
+ </UButton>
93
+ </div>
94
+ </div>
95
+ </template>
96
+ </UModal>
97
+ </template>
@@ -0,0 +1,356 @@
1
+ <template>
2
+ <div class="w-[280px]" :data-testid="languageListTestIds.root">
3
+ <div class="flex justify-between items-center p-4">
4
+ <h3 class="label-lg">
5
+ {{ $t('editor.languages.title') }}
6
+ </h3>
7
+
8
+ <UPopover :data-testid="languageListTestIds.addPopover">
9
+ <UButton
10
+ color="primary"
11
+ variant="ghost"
12
+ trailing-icon="ph:caret-down"
13
+ size="sm"
14
+ square
15
+ :data-testid="languageListTestIds.addButton"
16
+ >
17
+ {{ $t('editor.languages.add') }}
18
+ </UButton>
19
+
20
+ <template #content>
21
+ <UCommandPalette
22
+ default-value=""
23
+ @update:model-value="addLanguage($event.value)"
24
+ :placeholder="$t('common.search')"
25
+ :groups="languageGroups"
26
+ :data-testid="languageListTestIds.commandPalette"
27
+ />
28
+ </template>
29
+ </UPopover>
30
+ </div>
31
+ <div
32
+ class="px-4 py-2.5 cursor-pointer w-full hover:bg-secondary-100 flex justify-between items-center"
33
+ :class="{
34
+ 'bg-secondary-50': lang.id === language,
35
+ }"
36
+ v-for="lang in grouped.used"
37
+ :key="lang.id"
38
+ @click="language = lang.id"
39
+ :data-testid="`${languageListTestIds.languageItem}-${lang.id}`"
40
+ >
41
+ <div class="flex flex-col gap-0.5">
42
+ <p class="paragraph-sm">
43
+ {{ lang.name }}
44
+ </p>
45
+ <div class="flex items-center gap-1.5">
46
+ <span class="paragraph-xs">
47
+ {{
48
+ t('language_list.translated_percentage', {
49
+ percentage: getTranslationPercentage(lang.id),
50
+ })
51
+ }}
52
+ </span>
53
+ <Icon
54
+ v-if="getTranslationPercentage(lang.id) === 100"
55
+ name="ph:check-circle-fill"
56
+ class="text-success-500 w-4 h-4"
57
+ />
58
+ </div>
59
+ </div>
60
+ <div class="flex items-center gap-2">
61
+ <div v-if="hasLanguageChanges(lang.id)" class="flex items-center gap-1.5">
62
+ <Icon name="ph:arrow-clockwise" class="text-warning-700 w-4 h-4" />
63
+ <span class="paragraph-sm text-warning-700">
64
+ {{ getLanguageChangeCount(lang.id) }}
65
+ {{
66
+ getLanguageChangeCount(lang.id) === 1
67
+ ? t('language_list.change')
68
+ : t('language_list.changes')
69
+ }}
70
+ </span>
71
+ </div>
72
+ <UButton
73
+ size="sm"
74
+ :disabled="grouped.used.length <= 1 || lang.id === 'en' || lang.id === mainLanguageId"
75
+ @click.stop="messages[lang.id] && askDeleteMessage(messages[lang.id]!)"
76
+ color="primary"
77
+ variant="ghost"
78
+ icon="ph:trash"
79
+ :data-testid="`${languageListTestIds.deleteButton}-${lang.id}`"
80
+ />
81
+ </div>
82
+ </div>
83
+ </div>
84
+ </template>
85
+
86
+ <script setup lang="ts">
87
+ import { useLanguagesQuery } from '~/queries'
88
+ import type {
89
+ LanguageFromDb,
90
+ Message,
91
+ PostEntity,
92
+ RenderType,
93
+ WhatsappMessage,
94
+ EmailMessage,
95
+ } from '@dev.smartpricing/message-composer-utils/types'
96
+ import {
97
+ getWhatsappEmpty,
98
+ calculateWhatsappTranslationPercentage,
99
+ getEmailEmpty,
100
+ duplicateBlocksForNewLanguage,
101
+ calculateEmailTranslationPercentage,
102
+ } from '@dev.smartpricing/message-composer-utils/utils'
103
+ import { ref, computed } from 'vue'
104
+ import { useDeleteMessageMutation } from '~/queries'
105
+ import { useAppContextStore } from '@/stores/appContext'
106
+
107
+ const { data: languages } = useLanguagesQuery()
108
+ const appContext = useAppContextStore()
109
+
110
+ const { confirm } = useConfirm()
111
+ const { mutateAsync: deleteMessageMutation } = useDeleteMessageMutation()
112
+
113
+ const grouped = computed(() => {
114
+ const result: { available: LanguageFromDb[]; used: LanguageFromDb[] } = {
115
+ available: [],
116
+ used: [],
117
+ }
118
+ if (!languages.value) return result
119
+
120
+ for (const lang of languages.value) {
121
+ if (Object.keys(messages.value).includes(lang.id)) {
122
+ result.used.push(lang)
123
+ } else {
124
+ result.available.push(lang)
125
+ }
126
+ }
127
+
128
+ return result
129
+ })
130
+
131
+ const availableLanguages = computed(() => {
132
+ if (!languages.value) return { recommended: [], others: [] }
133
+
134
+ const used = Object.keys(messages.value)
135
+ const unused = languages.value.filter((lang) => !used.includes(lang.id))
136
+
137
+ const recommended = unused.filter((lang) => appContext.languages.includes(lang.id))
138
+ const others = unused.filter((lang) => !appContext.languages.includes(lang.id))
139
+
140
+ return { recommended, others }
141
+ })
142
+
143
+ const { t } = useI18n()
144
+
145
+ const languageGroups = computed(() => {
146
+ const groups = []
147
+
148
+ if (availableLanguages.value.recommended.length > 0) {
149
+ groups.push({
150
+ id: 'recommended',
151
+ label: t('editor.languages.recommended'),
152
+ items: availableLanguages.value.recommended.map((lang) => ({
153
+ value: lang.id,
154
+ label: lang.name,
155
+ })),
156
+ })
157
+ }
158
+
159
+ if (availableLanguages.value.others.length > 0) {
160
+ groups.push({
161
+ id: 'all',
162
+ label: t('editor.languages.all_languages'),
163
+ items: availableLanguages.value.others.map((lang) => ({
164
+ value: lang.id,
165
+ label: lang.name,
166
+ })),
167
+ })
168
+ }
169
+
170
+ return groups
171
+ })
172
+
173
+ const props = defineProps<{
174
+ renderType: RenderType
175
+ messageGroupId?: string | number
176
+ mainLanguageId: string
177
+ }>()
178
+
179
+ // Initialize change tracking if messageGroupId is provided
180
+ const changeTracking = props.messageGroupId ? useChangeTracking(props.messageGroupId) : null
181
+ const changeTrackingEmail = props.messageGroupId
182
+ ? useChangeTrackingEmail(props.messageGroupId)
183
+ : null
184
+
185
+ const messages = defineModel<Record<string, PostEntity<Message>>>('messages', {
186
+ required: true,
187
+ })
188
+
189
+ function askDeleteMessage(message: Message | PostEntity<Message>) {
190
+ const languageId = message.language_id
191
+
192
+ confirm({
193
+ title: t('dialogs.delete_message.title'),
194
+ message: t('dialogs.delete_message.message', { name: message.language_id }),
195
+ destructive: true,
196
+ confirmLabel: t('common.actions.delete'),
197
+ cancelLabel: t('common.actions.cancel'),
198
+ action: async () => {
199
+ if ('id' in message && 'message_group_id' in message) {
200
+ await deleteMessageMutation({
201
+ id: message.id,
202
+ messageGroupId: message.message_group_id,
203
+ })
204
+ }
205
+ useToast().add({
206
+ title: t('common.success'),
207
+ description: t('dialogs.delete_message.success', { name: message.language_id }),
208
+ color: 'success',
209
+ })
210
+ delete messages.value[message.language_id]
211
+ if (language.value === message.language_id) {
212
+ language.value = grouped.value.used.at(0)?.id as string
213
+ }
214
+ emit('removeLanguage', languageId)
215
+ },
216
+ })
217
+ }
218
+
219
+ const language = defineModel<string>('language', {
220
+ required: true,
221
+ })
222
+
223
+ const emit = defineEmits<{
224
+ addLanguage: [languageId: string]
225
+ removeLanguage: [languageId: string]
226
+ }>()
227
+
228
+ // Helper functions for change tracking
229
+ function hasLanguageChanges(languageId: string): boolean {
230
+ const message = messages.value[languageId]
231
+ if (!message) return false
232
+
233
+ if (message.render_type === 'whatsapp_v1' && changeTracking) {
234
+ return changeTracking.hasChanges(languageId, message as WhatsappMessage)
235
+ }
236
+ if (message.render_type === 'email_v1' && changeTrackingEmail) {
237
+ return changeTrackingEmail.hasChanges(languageId, message as EmailMessage)
238
+ }
239
+ return false
240
+ }
241
+
242
+ function getLanguageChangeCount(languageId: string): number {
243
+ const message = messages.value[languageId]
244
+ if (!message) return 0
245
+
246
+ if (message.render_type === 'whatsapp_v1' && changeTracking) {
247
+ return changeTracking.getChangeCount(languageId, message as WhatsappMessage)
248
+ }
249
+ if (message.render_type === 'email_v1' && changeTrackingEmail) {
250
+ return changeTrackingEmail.getChangeCount(languageId, message as EmailMessage)
251
+ }
252
+ return 0
253
+ }
254
+
255
+ function addLanguage(_language: string) {
256
+ switch (props.renderType) {
257
+ case 'whatsapp_v1': {
258
+ const newMessage = getWhatsappEmpty(_language)
259
+
260
+ // Copy structure from base message (main language)
261
+ const baseMessage = messages.value[props.mainLanguageId]
262
+ if (baseMessage && baseMessage.render_type === 'whatsapp_v1') {
263
+ const whatsappBase = baseMessage as PostEntity<WhatsappMessage>
264
+
265
+ // Copy header structure
266
+ if (whatsappBase.body.header.type === 'text') {
267
+ newMessage.body.header = {
268
+ type: 'text',
269
+ content: undefined,
270
+ }
271
+ } else if (whatsappBase.body.header.type !== 'none') {
272
+ // Copy media header as-is (editable per-language in translations)
273
+ newMessage.body.header = { ...whatsappBase.body.header }
274
+ }
275
+
276
+ // Copy footer enabled state
277
+ if (whatsappBase.body.footer.enabled) {
278
+ newMessage.body.footer = {
279
+ enabled: true,
280
+ content: '',
281
+ }
282
+ }
283
+
284
+ // Copy button structure
285
+ if (whatsappBase.body.buttons?.buttons) {
286
+ newMessage.body.buttons.buttons = whatsappBase.body.buttons.buttons.map((btn: any) => ({
287
+ ...btn,
288
+ text: '',
289
+ }))
290
+ newMessage.body.buttons.enabled = whatsappBase.body.buttons.enabled
291
+ }
292
+ }
293
+
294
+ messages.value[_language] = newMessage
295
+
296
+ // Record original state for the new language in change tracker
297
+ if (changeTracking) {
298
+ changeTracking.recordOriginalState(_language, newMessage as WhatsappMessage)
299
+ }
300
+
301
+ break
302
+ }
303
+ case 'email_v1': {
304
+ const newMessage = getEmailEmpty(_language)
305
+
306
+ // Copy structure from base message (main language)
307
+ const baseMessage = messages.value[props.mainLanguageId]
308
+ if (baseMessage && baseMessage.render_type === 'email_v1') {
309
+ const emailBase = baseMessage as PostEntity<EmailMessage>
310
+
311
+ // Duplicate blocks with new UUIDs and clear translatable content
312
+ newMessage.body.blocks = duplicateBlocksForNewLanguage(emailBase.body.blocks)
313
+ // Copy global settings (colors, fonts, etc.)
314
+ newMessage.body.settings = { ...emailBase.body.settings }
315
+ // Clear subject line (translatable)
316
+ newMessage.body.subject = ''
317
+ }
318
+
319
+ messages.value[_language] = newMessage
320
+
321
+ // Record original state for the new language in change tracker
322
+ if (changeTrackingEmail) {
323
+ changeTrackingEmail.recordOriginalState(_language, newMessage as EmailMessage)
324
+ }
325
+
326
+ break
327
+ }
328
+ }
329
+ language.value = _language
330
+ emit('addLanguage', _language)
331
+ }
332
+
333
+ function getTranslationPercentage(languageId: string): number {
334
+ const message = messages.value[languageId]
335
+ if (!message) return 0
336
+
337
+ // Use the first message as base language for comparison
338
+ const baseMessage = messages.value[props.mainLanguageId]
339
+ if (!baseMessage) return 0
340
+
341
+ switch (message.render_type) {
342
+ case 'whatsapp_v1':
343
+ return calculateWhatsappTranslationPercentage(
344
+ message as PostEntity<WhatsappMessage>,
345
+ baseMessage as PostEntity<WhatsappMessage>,
346
+ )
347
+ case 'email_v1':
348
+ return calculateEmailTranslationPercentage(
349
+ message as PostEntity<EmailMessage>,
350
+ baseMessage as PostEntity<EmailMessage>,
351
+ )
352
+ default:
353
+ return 0
354
+ }
355
+ }
356
+ </script>
@@ -0,0 +1,12 @@
1
+ <script setup lang="ts"></script>
2
+
3
+ <template>
4
+ <Teleport to="body">
5
+ <div
6
+ class="fixed isolate z-[9999] inset-0 bg-white/50 flex items-center justify-center backdrop-blur-[2px]"
7
+ :data-testid="loadingOverlayTestIds.root"
8
+ >
9
+ <div class="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-gray-900"></div>
10
+ </div>
11
+ </Teleport>
12
+ </template>