@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,126 @@
1
+ <script setup lang="ts">
2
+ import type { EmailMessage, PostEntity } from '@dev.smartpricing/message-composer-utils/types'
3
+ import { extractEmailParameters } from '@dev.smartpricing/message-composer-utils/utils'
4
+ import { ref, computed, watch } from 'vue'
5
+ import { useDebounceFn } from '@vueuse/core'
6
+ import HtmlPreviewIframe from '@/components/Common/HtmlPreviewIframe.vue'
7
+ import { useCompileEmailMutation } from '~/queries/compilation'
8
+
9
+ defineOptions({ name: 'EmailPreview' })
10
+
11
+ const props = withDefaults(
12
+ defineProps<{
13
+ message: PostEntity<EmailMessage> | EmailMessage
14
+ messageGroupId: string
15
+ dynamicData?: Record<string, string>
16
+ view?: 'desktop' | 'mobile'
17
+ showSubject?: boolean
18
+ brandId?: string
19
+ }>(),
20
+ { view: 'desktop', showSubject: false },
21
+ )
22
+
23
+ const iframeWidth = computed(() => {
24
+ return props.view === 'mobile' ? '400px' : '600px'
25
+ })
26
+
27
+ // Extract parameters directly from message
28
+ const parameters = computed(() => extractEmailParameters(props.message))
29
+
30
+ // Initialize parameter values
31
+ const parameterValues = ref<Record<string, string>>({})
32
+ const { mutate: compile, data: compiled, isLoading: isCompiling } = useCompileEmailMutation()
33
+
34
+ const compileEmail = useDebounceFn(() => {
35
+ compile({
36
+ messageGroupId: props.messageGroupId,
37
+ data: {
38
+ language: props.message.language_id,
39
+ parameters: parameterValues.value,
40
+ brand_id: props.brandId,
41
+ },
42
+ })
43
+ }, 0)
44
+
45
+ watch(
46
+ parameters,
47
+ (params) => {
48
+ const defaults = props.dynamicData || {}
49
+ parameterValues.value = params.reduce(
50
+ (acc, param) => {
51
+ acc[param] = defaults[param] || `Sample ${param}`
52
+ return acc
53
+ },
54
+ {} as Record<string, string>,
55
+ )
56
+
57
+ compileEmail()
58
+ },
59
+ { immediate: true },
60
+ )
61
+
62
+ watch(
63
+ () => props.brandId,
64
+ () => compileEmail(),
65
+ )
66
+
67
+ // Compilation
68
+ const iframeContent = ref<string>('')
69
+
70
+ // Update iframe when compilation succeeds
71
+ watch(
72
+ () => compiled.value,
73
+ (result) => {
74
+ if (result?.render_type === 'email_v1') {
75
+ iframeContent.value = result.html
76
+ }
77
+ },
78
+ )
79
+ </script>
80
+
81
+ <template>
82
+ <div
83
+ class="flex-1 bg-neutral-50 flex items-start justify-center overflow-auto"
84
+ :data-testid="emailPreviewTestIds.root"
85
+ >
86
+ <div
87
+ v-if="isCompiling"
88
+ class="space-y-2"
89
+ :style="{ width: iframeWidth }"
90
+ :data-testid="emailPreviewTestIds.loadingSkeleton"
91
+ >
92
+ <USkeleton class="h-6 w-1/3 bg-white" />
93
+ <USkeleton class="h-4 bg-white" />
94
+ <USkeleton class="h-4 w-5/8 bg-white" />
95
+ <USkeleton class="h-32 bg-white" />
96
+ <USkeleton class="h-12 bg-white" />
97
+ </div>
98
+
99
+ <div v-else-if="iframeContent" :style="{ width: iframeWidth }" class="relative overflow-auto">
100
+ <p
101
+ v-if="showSubject"
102
+ class="pb-4 bg-white mb-px sticky top-0"
103
+ :data-testid="emailPreviewTestIds.subject"
104
+ >
105
+ <span class="paragraph-sm-bold">{{ $t('email_preview.subject') }}:</span>
106
+ <span class="paragraph-sm">{{ message.body.subject }}</span>
107
+ </p>
108
+ <HtmlPreviewIframe
109
+ :html="iframeContent"
110
+ title="Email preview"
111
+ :min-height="600"
112
+ class="border-0 bg-white w-full"
113
+ :data-testid="emailPreviewTestIds.iframe"
114
+ />
115
+ </div>
116
+
117
+ <!-- Error State -->
118
+ <div
119
+ v-else
120
+ class="flex items-center justify-center text-gray-500"
121
+ :data-testid="emailPreviewTestIds.emptyState"
122
+ >
123
+ <p>{{ $t('email_preview.no_preview') }}</p>
124
+ </div>
125
+ </div>
126
+ </template>
@@ -0,0 +1,57 @@
1
+ <script setup lang="ts">
2
+ import type { EmailMessage, PostEntity } from '@dev.smartpricing/message-composer-utils/types'
3
+ import { ref } from 'vue'
4
+ import EmailPreview from './Preview.vue'
5
+ import SendTestEmailModal from './SendTestEmailModal.vue'
6
+ import type { TabsItem } from '#ui/types'
7
+
8
+ defineOptions({ name: 'EmailPreviewPanel' })
9
+
10
+ defineProps<{
11
+ message: PostEntity<EmailMessage> | EmailMessage
12
+ messageGroupId: string
13
+ dynamicData?: Record<string, string>
14
+ }>()
15
+
16
+ const emit = defineEmits<{
17
+ 'test-sent': [payload: { to_recipients: number }]
18
+ }>()
19
+
20
+ // Desktop/Mobile toggle
21
+ const { t } = useI18n()
22
+ const tabs: TabsItem[] = [
23
+ { label: t('common.desktop'), value: 'desktop' },
24
+ { label: t('common.mobile'), value: 'mobile' },
25
+ ] as const
26
+
27
+ const currentView = ref<(typeof tabs)[number]['value']>('desktop')
28
+ </script>
29
+
30
+ <template>
31
+ <div class="bg-white flex flex-col w-full h-full" :data-testid="emailPreviewPanelTestIds.root">
32
+ <!-- Desktop/Mobile Toggle -->
33
+ <div class="grid grid-cols-3 gap-2 border-b border-neutral-200 p-2">
34
+ <div></div>
35
+ <div class="flex justify-center items-center">
36
+ <UTabs
37
+ size="xs"
38
+ variant="pill"
39
+ :items="tabs"
40
+ v-model="currentView"
41
+ :ui="{ root: 'gap-0 w-full' }"
42
+ :data-testid="emailPreviewPanelTestIds.viewTabs"
43
+ />
44
+ </div>
45
+ <div class="flex justify-end items-center">
46
+ <SendTestEmailModal :message="message" @test-sent="emit('test-sent', $event)" />
47
+ </div>
48
+ </div>
49
+
50
+ <EmailPreview
51
+ :message="message"
52
+ :message-group-id="messageGroupId"
53
+ :dynamic-data="dynamicData"
54
+ :view="currentView"
55
+ />
56
+ </div>
57
+ </template>
@@ -0,0 +1,121 @@
1
+ <script setup lang="ts">
2
+ import type { PostEntity, EmailMessage } from '@dev.smartpricing/message-composer-utils/types'
3
+ import { isEmail } from '@dev.smartpricing/message-composer-utils/utils'
4
+ import { useSendTestEmailMutation } from '~/queries'
5
+ import { z } from 'zod'
6
+
7
+ defineOptions({
8
+ name: 'SendTestEmailModal',
9
+ })
10
+
11
+ const props = defineProps<{
12
+ message: PostEntity<EmailMessage>
13
+ }>()
14
+
15
+ const emit = defineEmits<{
16
+ 'test-sent': [payload: { to_recipients: number }]
17
+ }>()
18
+
19
+ const { t } = useI18n()
20
+ const toast = useToast()
21
+ const { mutate: sendTest, isLoading: isSending, status } = useSendTestEmailMutation()
22
+
23
+ const state = ref<Schema>({
24
+ to: '',
25
+ })
26
+
27
+ const schema = z.object({
28
+ to: z
29
+ .string()
30
+ .min(1, t('editor.validation.error.emailRequired'))
31
+ .refine(
32
+ (value) => {
33
+ const emails = value
34
+ .split(';')
35
+ .map((e) => e.trim())
36
+ .filter(Boolean)
37
+ return emails.length > 0 && emails.every((email) => isEmail(email))
38
+ },
39
+ { message: t('editor.validation.error.emailInvalid') },
40
+ ),
41
+ })
42
+
43
+ type Schema = z.output<typeof schema>
44
+
45
+ function onSubmit() {
46
+ sendTest({
47
+ message: props.message,
48
+ to: state.value.to,
49
+ })
50
+ }
51
+
52
+ const open = ref(false)
53
+ const formRef = ref()
54
+
55
+ watch(status, (newStatus) => {
56
+ if (newStatus === 'success') {
57
+ const recipients = state.value.to
58
+ .split(';')
59
+ .map((e) => e.trim())
60
+ .filter(Boolean).length
61
+ emit('test-sent', { to_recipients: recipients })
62
+ toast.add({
63
+ title: t('common.success'),
64
+ description: t('pages.email.send_test_success'),
65
+ color: 'success',
66
+ })
67
+ open.value = false
68
+ }
69
+ })
70
+ </script>
71
+
72
+ <template>
73
+ <UModal
74
+ :title="$t('pages.email.send_test')"
75
+ :ui="{ footer: 'justify-end' }"
76
+ v-model:open="open"
77
+ close-icon="ph:x"
78
+ :close="{ 'data-testid': sendTestEmailModalTestIds.modalCloseButton }"
79
+ :data-testid="sendTestEmailModalTestIds.modal"
80
+ >
81
+ <UButton
82
+ icon="ph:envelope-simple-bold"
83
+ :label="$t('pages.email.send_test')"
84
+ size="md"
85
+ color="primary"
86
+ variant="ghost"
87
+ :data-testid="sendTestEmailModalTestIds.triggerButton"
88
+ />
89
+
90
+ <template #body>
91
+ <UForm ref="formRef" :schema="schema" :state="state" @submit="onSubmit">
92
+ <UFormField name="to" :data-testid="sendTestEmailModalTestIds.toFormField">
93
+ <UInput
94
+ v-model="state.to"
95
+ :placeholder="$t('pages.email.test_email_to_placeholder')"
96
+ :data-testid="sendTestEmailModalTestIds.toInput"
97
+ />
98
+ </UFormField>
99
+ </UForm>
100
+ </template>
101
+
102
+ <template #footer>
103
+ <UButton
104
+ variant="outline"
105
+ :label="$t('common.actions.cancel')"
106
+ size="sm"
107
+ color="primary"
108
+ @click="open = false"
109
+ :data-testid="sendTestEmailModalTestIds.cancelButton"
110
+ />
111
+ <UButton
112
+ :label="$t('pages.email.send_test')"
113
+ size="sm"
114
+ color="primary"
115
+ :loading="isSending"
116
+ @click="formRef.submit()"
117
+ :data-testid="sendTestEmailModalTestIds.sendButton"
118
+ />
119
+ </template>
120
+ </UModal>
121
+ </template>
@@ -0,0 +1,198 @@
1
+ <script setup lang="ts">
2
+ import { ref, computed, watch } from 'vue'
3
+ import type { EmailMessage, PostEntity } from '@dev.smartpricing/message-composer-utils/types'
4
+ import { useI18n } from 'vue-i18n'
5
+ import { getLanguageDisplayName } from '@dev.smartpricing/message-composer-utils/utils'
6
+ import EmailTranslator from './Translator/Index.vue'
7
+ import TranslationActionBar from '@/components/Common/TranslationActionBar.vue'
8
+
9
+ defineOptions({
10
+ name: 'EmailTranslationEditor',
11
+ })
12
+
13
+ const { baseMessage, currentLanguage, baseLanguage, messageGroupId, messages } = defineProps<{
14
+ baseMessage: EmailMessage | PostEntity<EmailMessage>
15
+ currentLanguage: string
16
+ baseLanguage: string
17
+ messageGroupId?: string | number
18
+ messages: Record<string, EmailMessage | PostEntity<EmailMessage>>
19
+ }>()
20
+
21
+ const emit = defineEmits<{
22
+ 'auto-translate-tracked': [language: string]
23
+ preview: [language: string]
24
+ 'filters-changed': [
25
+ payload: {
26
+ language: string
27
+ search_term: string
28
+ show_only_missing: boolean
29
+ show_only_changes: boolean
30
+ },
31
+ ]
32
+ }>()
33
+
34
+ const targetMessage = defineModel<EmailMessage | PostEntity<EmailMessage>>({
35
+ required: true,
36
+ })
37
+
38
+ const { t, locale } = useI18n()
39
+
40
+ // Search and filter state
41
+ const searchQuery = ref('')
42
+ const showOnlyMissing = ref(false)
43
+ const showOnlyChanges = ref(false)
44
+
45
+ // Auto-translate composable
46
+ const { translateMessage, isTranslating } = useAutoTranslateEmail()
47
+
48
+ // Translation filters
49
+ const messageGroupIdRef = computed(() => messageGroupId)
50
+ const currentLanguageRef = computed(() => currentLanguage)
51
+ const baseLanguageRef = computed(() => baseLanguage)
52
+ const messagesRef = computed(() => messages)
53
+
54
+ const { filteredBlockIds, showSubject, hasActiveFilters } = useTranslationFiltersEmail({
55
+ messages: messagesRef,
56
+ currentLanguage: currentLanguageRef,
57
+ baseLanguage: baseLanguageRef,
58
+ messageGroupId: messageGroupIdRef,
59
+ searchQuery,
60
+ showOnlyMissing,
61
+ showOnlyChanges,
62
+ })
63
+
64
+ // Auto-translate handler
65
+ async function handleAutoTranslate() {
66
+ emit('auto-translate-tracked', currentLanguage)
67
+
68
+ await translateMessage(baseMessage, targetMessage.value, baseLanguage, currentLanguage)
69
+ }
70
+
71
+ function handlePreview() {
72
+ emit('preview', currentLanguage)
73
+ }
74
+
75
+ // Emit filters-changed when any filter changes
76
+ watch([searchQuery, showOnlyMissing, showOnlyChanges], () => {
77
+ emit('filters-changed', {
78
+ language: currentLanguage,
79
+ search_term: searchQuery.value,
80
+ show_only_missing: showOnlyMissing.value,
81
+ show_only_changes: showOnlyChanges.value,
82
+ })
83
+ })
84
+
85
+ // Clear filters
86
+ function clearFilters() {
87
+ searchQuery.value = ''
88
+ showOnlyMissing.value = false
89
+ showOnlyChanges.value = false
90
+ }
91
+ </script>
92
+
93
+ <template>
94
+ <div class="flex flex-col relative" :data-testid="emailTranslationEditorTestIds.root">
95
+ <!-- Translation action bar -->
96
+ <TranslationActionBar
97
+ :messages="messages"
98
+ :current-language="currentLanguage"
99
+ :base-language="baseLanguage"
100
+ v-model:search-query="searchQuery"
101
+ v-model:show-only-missing="showOnlyMissing"
102
+ v-model:show-only-changes="showOnlyChanges"
103
+ :is-translating="isTranslating"
104
+ @auto-translate="handleAutoTranslate"
105
+ @preview="handlePreview"
106
+ />
107
+
108
+ <div class="p-4 space-y-2">
109
+ <!-- Subject line translation card -->
110
+ <div
111
+ v-if="showSubject"
112
+ class="bg-white border border-muted rounded-lg overflow-hidden"
113
+ :data-testid="emailTranslationEditorTestIds.subjectCard"
114
+ >
115
+ <!-- Card header -->
116
+ <div class="px-4 py-3 border-b border-muted">
117
+ <h4 class="label-sm">
118
+ {{ t('pages.email.edit_message_group.subject') }}
119
+ </h4>
120
+ </div>
121
+
122
+ <!-- Card content -->
123
+ <div class="px-px">
124
+ <!-- Column headers -->
125
+ <div class="bg-muted px-4 py-2 flex gap-8">
126
+ <div class="flex-1 min-w-0">
127
+ <div class="flex items-center gap-1">
128
+ <p class="label-xs text-primary-700">
129
+ {{ getLanguageDisplayName(baseLanguage, locale) }}
130
+ </p>
131
+ </div>
132
+ </div>
133
+ <div class="flex-1 min-w-0">
134
+ <div class="flex items-center gap-1">
135
+ <p class="label-xs text-primary-700">
136
+ {{ getLanguageDisplayName(currentLanguage, locale) }}
137
+ </p>
138
+ </div>
139
+ </div>
140
+ </div>
141
+
142
+ <!-- Content row -->
143
+ <div class="px-4 py-3">
144
+ <div class="flex gap-2 items-start">
145
+ <!-- Base subject (read-only) -->
146
+ <div class="flex-1 min-w-0">
147
+ <div
148
+ class="paragraph-sm text-primary-700 break-words"
149
+ v-text="baseMessage.body.subject"
150
+ :data-testid="emailTranslationEditorTestIds.baseSubjectPreview"
151
+ />
152
+ </div>
153
+
154
+ <!-- Target subject (editable) -->
155
+ <div class="flex-1 min-w-0">
156
+ <UInput
157
+ v-model="targetMessage.body.subject"
158
+ :placeholder="t('pages.email.edit_message_group.subject_placeholder')"
159
+ :data-testid="emailTranslationEditorTestIds.targetSubjectInput"
160
+ />
161
+ </div>
162
+ </div>
163
+ </div>
164
+ </div>
165
+ </div>
166
+
167
+ <!-- Empty State when no sections match filters -->
168
+ <UEmpty
169
+ v-if="hasActiveFilters && filteredBlockIds.length === 0 && !showSubject"
170
+ icon="ph:funnel"
171
+ :title="$t('translation_action_bar.no_sections_found')"
172
+ :data-testid="emailTranslationEditorTestIds.emptyState"
173
+ >
174
+ <template #actions>
175
+ <UButton
176
+ color="neutral"
177
+ variant="outline"
178
+ size="sm"
179
+ @click="clearFilters"
180
+ :data-testid="emailTranslationEditorTestIds.clearFiltersButton"
181
+ >
182
+ {{ $t('translation_action_bar.clear_filters') }}
183
+ </UButton>
184
+ </template>
185
+ </UEmpty>
186
+
187
+ <!-- Email translator -->
188
+ <EmailTranslator
189
+ v-if="!hasActiveFilters || filteredBlockIds.length > 0"
190
+ :base-message="baseMessage"
191
+ v-model="targetMessage"
192
+ :base-language="baseLanguage"
193
+ :target-language="currentLanguage"
194
+ :filtered-block-ids="filteredBlockIds"
195
+ />
196
+ </div>
197
+ </div>
198
+ </template>
@@ -0,0 +1,101 @@
1
+ <script setup lang="ts">
2
+ import type { JSONContent } from '@tiptap/vue-3'
3
+ import { generateHTML } from '@tiptap/vue-3'
4
+ import { getTipTapExtensions, extractText } from '@dev.smartpricing/message-composer-utils/utils'
5
+ import RichTextEditor from '@/components/Editor/RichTextEditor.vue'
6
+ import { useAppContextStore } from '@/stores/appContext'
7
+ import type { ButtonBlock } from '@dev.smartpricing/message-composer-utils/types'
8
+
9
+ defineOptions({
10
+ name: 'EmailTranslatorButton',
11
+ })
12
+
13
+ const { baseBlock } = defineProps<{
14
+ baseBlock: ButtonBlock
15
+ }>()
16
+
17
+ const targetBlock = defineModel<ButtonBlock>({
18
+ required: true,
19
+ })
20
+
21
+ const appContext = useAppContextStore()
22
+
23
+ // Generate HTML for preview of base language
24
+ function generatePreviewHtml(content: JSONContent | undefined): string {
25
+ if (!content) return ''
26
+ return generateHTML(content, getTipTapExtensions({ preset: 'email' })).replaceAll(
27
+ '<p></p>',
28
+ '<p>&nbsp;</p>',
29
+ )
30
+ }
31
+
32
+ // Copy URL to clipboard
33
+ async function copyUrl() {
34
+ if (baseBlock.settings.url) {
35
+ await navigator.clipboard.writeText(baseBlock.settings.url)
36
+ }
37
+ }
38
+ </script>
39
+
40
+ <template>
41
+ <div class="flex gap-8 items-start" :data-testid="emailTranslatorButtonTestIds.root">
42
+ <!-- Base language preview (read-only) -->
43
+ <div class="flex-1 min-w-0 space-y-2">
44
+ <div
45
+ class="paragraph-sm text-primary-700 break-words"
46
+ v-html="generatePreviewHtml(baseBlock.settings.content)"
47
+ :data-testid="emailTranslatorButtonTestIds.basePreview"
48
+ />
49
+
50
+ <!-- URL display with copy button -->
51
+ <div v-if="baseBlock.settings.url" class="flex items-center gap-2">
52
+ <UBadge
53
+ variant="subtle"
54
+ color="neutral"
55
+ size="sm"
56
+ :data-testid="emailTranslatorButtonTestIds.linkTypeBadge"
57
+ >
58
+ {{ baseBlock.settings.linkType }}
59
+ </UBadge>
60
+ <code class="text-xs text-gray-600 truncate max-w-xs">{{ baseBlock.settings.url }}</code>
61
+ <UButton
62
+ icon="i-lucide-copy"
63
+ variant="ghost"
64
+ size="xs"
65
+ @click="copyUrl"
66
+ :data-testid="emailTranslatorButtonTestIds.copyUrlButton"
67
+ />
68
+ </div>
69
+ </div>
70
+
71
+ <!-- Translation editor (editable) -->
72
+ <div class="flex-1 min-w-0 space-y-2">
73
+ <div class="flex gap-2 items-start">
74
+ <div class="flex-1">
75
+ <RichTextEditor
76
+ v-model:text="targetBlock.settings.content"
77
+ preset="email"
78
+ bold
79
+ italic
80
+ underline
81
+ lists
82
+ horizontal-rule
83
+ alignment
84
+ link
85
+ emoji
86
+ dynamic-values
87
+ :data-testid="emailTranslatorButtonTestIds.contentEditor"
88
+ />
89
+ </div>
90
+ <slot name="actions" />
91
+ </div>
92
+
93
+ <!-- URL input for translation -->
94
+ <UInput
95
+ v-model="targetBlock.settings.url"
96
+ :placeholder="baseBlock.settings.url || 'https://'"
97
+ :data-testid="emailTranslatorButtonTestIds.urlInput"
98
+ />
99
+ </div>
100
+ </div>
101
+ </template>
@@ -0,0 +1,60 @@
1
+ <script setup lang="ts">
2
+ import type { JSONContent } from '@tiptap/vue-3'
3
+ import { generateHTML } from '@tiptap/vue-3'
4
+ import { getTipTapExtensions } from '@dev.smartpricing/message-composer-utils/utils'
5
+ import RichTextEditor from '@/components/Editor/RichTextEditor.vue'
6
+ import { useAppContextStore } from '@/stores/appContext'
7
+ import type { DiscountBlock } from '@dev.smartpricing/message-composer-utils/types'
8
+
9
+ defineOptions({
10
+ name: 'EmailTranslatorDiscount',
11
+ })
12
+
13
+ const { baseBlock } = defineProps<{
14
+ baseBlock: DiscountBlock
15
+ }>()
16
+
17
+ const targetBlock = defineModel<DiscountBlock>({
18
+ required: true,
19
+ })
20
+
21
+ const appContext = useAppContextStore()
22
+
23
+ // Generate HTML for preview of base language
24
+ function generatePreviewHtml(content: JSONContent | undefined): string {
25
+ if (!content) return ''
26
+ return generateHTML(content, getTipTapExtensions({ preset: 'email' })).replaceAll(
27
+ '<p></p>',
28
+ '<p>&nbsp;</p>',
29
+ )
30
+ }
31
+ </script>
32
+
33
+ <template>
34
+ <div class="flex gap-8 items-start" :data-testid="emailTranslatorDiscountTestIds.root">
35
+ <!-- Base language preview (read-only) -->
36
+ <div class="flex-1 min-w-0">
37
+ <div
38
+ class="paragraph-sm text-primary-700 break-words"
39
+ v-html="generatePreviewHtml(baseBlock.settings.content)"
40
+ :data-testid="emailTranslatorDiscountTestIds.basePreview"
41
+ />
42
+ </div>
43
+
44
+ <!-- Translation editor (editable) -->
45
+ <div class="flex-1 min-w-0 flex gap-2 items-start">
46
+ <div class="flex-1">
47
+ <RichTextEditor
48
+ v-model:text="targetBlock.settings.content"
49
+ preset="email"
50
+ bold
51
+ alignment
52
+ dynamic-values
53
+ :data-testid="emailTranslatorDiscountTestIds.contentEditor"
54
+ />
55
+ </div>
56
+
57
+ <slot name="actions" />
58
+ </div>
59
+ </div>
60
+ </template>