@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,66 @@
|
|
|
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 { FooterBlock } from '@dev.smartpricing/message-composer-utils/types'
|
|
8
|
+
|
|
9
|
+
defineOptions({
|
|
10
|
+
name: 'EmailTranslatorFooter',
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
const { baseBlock } = defineProps<{
|
|
14
|
+
baseBlock: FooterBlock
|
|
15
|
+
}>()
|
|
16
|
+
|
|
17
|
+
const targetBlock = defineModel<FooterBlock>({
|
|
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> </p>',
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<template>
|
|
34
|
+
<div class="flex gap-8 items-start" :data-testid="emailTranslatorFooterTestIds.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="emailTranslatorFooterTestIds.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
|
+
italic
|
|
52
|
+
underline
|
|
53
|
+
lists
|
|
54
|
+
horizontal-rule
|
|
55
|
+
alignment
|
|
56
|
+
link
|
|
57
|
+
emoji
|
|
58
|
+
dynamic-values
|
|
59
|
+
:data-testid="emailTranslatorFooterTestIds.contentEditor"
|
|
60
|
+
/>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<slot name="actions" />
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
</template>
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import type { GridBlock, EmailBlock } from '@dev.smartpricing/message-composer-utils/types'
|
|
4
|
+
import HeadingTranslator from './Heading.vue'
|
|
5
|
+
import ParagraphTranslator from './Paragraph.vue'
|
|
6
|
+
import ButtonTranslator from './Button.vue'
|
|
7
|
+
import ImageTranslator from './Image.vue'
|
|
8
|
+
|
|
9
|
+
defineOptions({
|
|
10
|
+
name: 'EmailTranslatorGrid',
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
const { baseBlock, blockIndex } = defineProps<{
|
|
14
|
+
baseBlock: GridBlock
|
|
15
|
+
targetBlock: GridBlock
|
|
16
|
+
blockIndex?: number
|
|
17
|
+
}>()
|
|
18
|
+
|
|
19
|
+
const targetBlock = defineModel<GridBlock>({
|
|
20
|
+
required: true,
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
// Component registry for nested translators
|
|
24
|
+
const translatorComponents: Record<string, any> = {
|
|
25
|
+
heading: HeadingTranslator,
|
|
26
|
+
paragraph: ParagraphTranslator,
|
|
27
|
+
button: ButtonTranslator,
|
|
28
|
+
image: ImageTranslator,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Extract all translatable nested blocks with their positions
|
|
32
|
+
interface NestedBlockInfo {
|
|
33
|
+
columnIndex: number
|
|
34
|
+
blockIndex: number
|
|
35
|
+
baseBlock: EmailBlock
|
|
36
|
+
targetBlock: EmailBlock
|
|
37
|
+
label: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const nestedTranslatableBlocks = computed<NestedBlockInfo[]>(() => {
|
|
41
|
+
const blocks: NestedBlockInfo[] = []
|
|
42
|
+
|
|
43
|
+
// Build ID-based map of target columns
|
|
44
|
+
const targetColumnsMap = new Map(
|
|
45
|
+
targetBlock.value.settings.columnData.map((col) => [col.id, col]),
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
for (let colIdx = 0; colIdx < baseBlock.settings.columnData.length; colIdx++) {
|
|
49
|
+
const baseColumn = baseBlock.settings.columnData[colIdx]
|
|
50
|
+
if (!baseColumn) continue
|
|
51
|
+
const targetColumn = targetColumnsMap.get(baseColumn.id)
|
|
52
|
+
|
|
53
|
+
if (!targetColumn) continue
|
|
54
|
+
|
|
55
|
+
// Build ID-based map of target nested blocks within this column
|
|
56
|
+
const targetNestedMap = new Map(targetColumn.blocks.map((b) => [b.id, b]))
|
|
57
|
+
|
|
58
|
+
for (let blkIdx = 0; blkIdx < baseColumn.blocks.length; blkIdx++) {
|
|
59
|
+
const baseNested = baseColumn.blocks[blkIdx]
|
|
60
|
+
if (!baseNested) continue
|
|
61
|
+
const targetNested = targetNestedMap.get(baseNested.id)
|
|
62
|
+
|
|
63
|
+
if (!targetNested) continue
|
|
64
|
+
|
|
65
|
+
// Only include translatable blocks
|
|
66
|
+
if (
|
|
67
|
+
baseNested.type === 'heading' ||
|
|
68
|
+
baseNested.type === 'paragraph' ||
|
|
69
|
+
baseNested.type === 'button' ||
|
|
70
|
+
baseNested.type === 'image'
|
|
71
|
+
) {
|
|
72
|
+
const gridLabel = blockIndex !== undefined ? `Grid ${blockIndex + 1}` : 'Grid'
|
|
73
|
+
blocks.push({
|
|
74
|
+
columnIndex: colIdx,
|
|
75
|
+
blockIndex: blkIdx,
|
|
76
|
+
baseBlock: baseNested,
|
|
77
|
+
targetBlock: targetNested,
|
|
78
|
+
label: `${gridLabel} → Column ${colIdx + 1} → ${baseNested.type}`,
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return blocks
|
|
85
|
+
})
|
|
86
|
+
</script>
|
|
87
|
+
|
|
88
|
+
<template>
|
|
89
|
+
<div class="space-y-6" :data-testid="emailTranslatorGridTestIds.root">
|
|
90
|
+
<div
|
|
91
|
+
v-for="(info, idx) in nestedTranslatableBlocks"
|
|
92
|
+
:key="`${info.columnIndex}-${info.blockIndex}`"
|
|
93
|
+
class="space-y-2"
|
|
94
|
+
:data-testid="`${emailTranslatorGridTestIds.nestedBlock}-${info.columnIndex}-${info.blockIndex}`"
|
|
95
|
+
>
|
|
96
|
+
<UBadge
|
|
97
|
+
variant="subtle"
|
|
98
|
+
color="neutral"
|
|
99
|
+
size="sm"
|
|
100
|
+
:data-testid="`${emailTranslatorGridTestIds.nestedBlockBadge}-${info.columnIndex}-${info.blockIndex}`"
|
|
101
|
+
>
|
|
102
|
+
{{ info.label }}
|
|
103
|
+
</UBadge>
|
|
104
|
+
|
|
105
|
+
<component
|
|
106
|
+
:is="translatorComponents[info.baseBlock.type]"
|
|
107
|
+
v-if="translatorComponents[info.baseBlock.type]"
|
|
108
|
+
:base-block="info.baseBlock"
|
|
109
|
+
v-model="info.targetBlock"
|
|
110
|
+
>
|
|
111
|
+
<template #actions>
|
|
112
|
+
<slot name="actions" :block="info" />
|
|
113
|
+
</template>
|
|
114
|
+
</component>
|
|
115
|
+
|
|
116
|
+
<UDivider v-if="idx < nestedTranslatableBlocks.length - 1" />
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
<div
|
|
120
|
+
v-if="nestedTranslatableBlocks.length === 0"
|
|
121
|
+
class="text-gray-500 text-sm italic"
|
|
122
|
+
:data-testid="emailTranslatorGridTestIds.emptyState"
|
|
123
|
+
>
|
|
124
|
+
No translatable blocks in grid
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
</template>
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { HeaderBlock } from '@dev.smartpricing/message-composer-utils/types'
|
|
3
|
+
import { computed } from 'vue'
|
|
4
|
+
import { useI18n } from 'vue-i18n'
|
|
5
|
+
import MediaUploadOrSelect from '@/components/Common/MediaUploadOrSelect.vue'
|
|
6
|
+
import { deepClone } from '@dev.smartpricing/message-composer-utils/utils'
|
|
7
|
+
|
|
8
|
+
defineOptions({
|
|
9
|
+
name: 'EmailTranslatorHeader',
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
const { baseBlock } = defineProps<{
|
|
13
|
+
baseBlock: HeaderBlock
|
|
14
|
+
}>()
|
|
15
|
+
|
|
16
|
+
const targetBlock = defineModel<HeaderBlock>({ required: true })
|
|
17
|
+
|
|
18
|
+
const { t } = useI18n()
|
|
19
|
+
|
|
20
|
+
const isCustom = computed(() => targetBlock.value.settings.src !== baseBlock.settings.src)
|
|
21
|
+
|
|
22
|
+
function resetToMain() {
|
|
23
|
+
const resetBlock = deepClone(toRaw(baseBlock))
|
|
24
|
+
targetBlock.value.settings.src = resetBlock.settings.src
|
|
25
|
+
targetBlock.value.settings.alt = resetBlock.settings.alt
|
|
26
|
+
}
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<template>
|
|
30
|
+
<div class="space-y-3" :data-testid="emailTranslatorHeaderTestIds.root">
|
|
31
|
+
<!-- Image row -->
|
|
32
|
+
<div class="flex gap-8 items-start">
|
|
33
|
+
<!-- Base language preview (read-only) -->
|
|
34
|
+
<div class="flex-1 min-w-0">
|
|
35
|
+
<div v-if="baseBlock.settings.src" class="max-w-[200px]">
|
|
36
|
+
<img
|
|
37
|
+
:src="baseBlock.settings.src"
|
|
38
|
+
class="rounded border border-muted max-h-[120px] object-contain"
|
|
39
|
+
:data-testid="emailTranslatorHeaderTestIds.basePreviewImage"
|
|
40
|
+
/>
|
|
41
|
+
</div>
|
|
42
|
+
<div v-else class="paragraph-sm text-muted">
|
|
43
|
+
{{ t('editor.email.blocks.header.name') }}
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<!-- Translation editor (editable) -->
|
|
48
|
+
<div class="flex-1 min-w-0">
|
|
49
|
+
<div class="flex items-center gap-2">
|
|
50
|
+
<MediaUploadOrSelect
|
|
51
|
+
v-model="targetBlock.settings.src"
|
|
52
|
+
media-type="image"
|
|
53
|
+
class="flex-1"
|
|
54
|
+
show-preview
|
|
55
|
+
:data-testid="emailTranslatorHeaderTestIds.mediaUpload"
|
|
56
|
+
>
|
|
57
|
+
<template #actions>
|
|
58
|
+
<UTooltip :text="t('editor.whatsapp.translator.reset_to_main')" v-if="isCustom">
|
|
59
|
+
<UButton
|
|
60
|
+
color="warning"
|
|
61
|
+
variant="ghost"
|
|
62
|
+
icon="ph:arrow-counter-clockwise-thin"
|
|
63
|
+
@click.stop="resetToMain"
|
|
64
|
+
:data-testid="emailTranslatorHeaderTestIds.resetButton"
|
|
65
|
+
/>
|
|
66
|
+
</UTooltip>
|
|
67
|
+
</template>
|
|
68
|
+
</MediaUploadOrSelect>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<!-- Alt text row -->
|
|
74
|
+
<div class="flex gap-8 items-start" v-if="baseBlock.settings.alt">
|
|
75
|
+
<div class="flex-1 min-w-0">
|
|
76
|
+
<p class="paragraph-sm text-muted truncate">
|
|
77
|
+
{{ baseBlock.settings.alt || '' }}
|
|
78
|
+
</p>
|
|
79
|
+
</div>
|
|
80
|
+
<div class="flex-1 min-w-0">
|
|
81
|
+
<UInput
|
|
82
|
+
v-model="targetBlock.settings.alt"
|
|
83
|
+
:placeholder="t('editor.email.blocks.header.settings.alt_placeholder')"
|
|
84
|
+
size="sm"
|
|
85
|
+
:data-testid="emailTranslatorHeaderTestIds.altInput"
|
|
86
|
+
/>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
</template>
|
|
@@ -0,0 +1,66 @@
|
|
|
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 { HeadingBlock } from '@dev.smartpricing/message-composer-utils/types'
|
|
8
|
+
|
|
9
|
+
defineOptions({
|
|
10
|
+
name: 'EmailTranslatorHeading',
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
const { baseBlock } = defineProps<{
|
|
14
|
+
baseBlock: HeadingBlock
|
|
15
|
+
}>()
|
|
16
|
+
|
|
17
|
+
const targetBlock = defineModel<HeadingBlock>({
|
|
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> </p>',
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<template>
|
|
34
|
+
<div class="flex gap-8 items-start" :data-testid="emailTranslatorHeadingTestIds.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="emailTranslatorHeadingTestIds.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
|
+
italic
|
|
52
|
+
underline
|
|
53
|
+
lists
|
|
54
|
+
horizontal-rule
|
|
55
|
+
alignment
|
|
56
|
+
link
|
|
57
|
+
emoji
|
|
58
|
+
dynamic-values
|
|
59
|
+
:data-testid="emailTranslatorHeadingTestIds.contentEditor"
|
|
60
|
+
/>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<slot name="actions" />
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
</template>
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { ImageBlock } from '@dev.smartpricing/message-composer-utils/types'
|
|
3
|
+
import { computed } from 'vue'
|
|
4
|
+
import { useI18n } from 'vue-i18n'
|
|
5
|
+
import MediaUploadOrSelect from '@/components/Common/MediaUploadOrSelect.vue'
|
|
6
|
+
import { deepClone } from '@dev.smartpricing/message-composer-utils/utils'
|
|
7
|
+
|
|
8
|
+
defineOptions({
|
|
9
|
+
name: 'EmailTranslatorImage',
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
const { baseBlock } = defineProps<{
|
|
13
|
+
baseBlock: ImageBlock
|
|
14
|
+
}>()
|
|
15
|
+
|
|
16
|
+
const targetBlock = defineModel<ImageBlock>({ required: true })
|
|
17
|
+
|
|
18
|
+
const { t } = useI18n()
|
|
19
|
+
|
|
20
|
+
const isCustom = computed(() => targetBlock.value.settings.src !== baseBlock.settings.src)
|
|
21
|
+
|
|
22
|
+
function resetToMain() {
|
|
23
|
+
const resetBlock = deepClone(toRaw(baseBlock))
|
|
24
|
+
targetBlock.value.settings.src = resetBlock.settings.src
|
|
25
|
+
targetBlock.value.settings.href = resetBlock.settings.href
|
|
26
|
+
}
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<template>
|
|
30
|
+
<div class="space-y-3" :data-testid="emailTranslatorImageTestIds.root">
|
|
31
|
+
<!-- Image row -->
|
|
32
|
+
<div class="flex gap-8 items-start">
|
|
33
|
+
<!-- Base language preview (read-only) -->
|
|
34
|
+
<div class="flex-1 min-w-0">
|
|
35
|
+
<div v-if="baseBlock.settings.src" class="max-w-[200px]">
|
|
36
|
+
<img
|
|
37
|
+
:src="baseBlock.settings.src"
|
|
38
|
+
class="rounded border border-muted max-h-[120px] object-contain"
|
|
39
|
+
:data-testid="emailTranslatorImageTestIds.basePreviewImage"
|
|
40
|
+
/>
|
|
41
|
+
</div>
|
|
42
|
+
<div v-else class="paragraph-sm text-muted">
|
|
43
|
+
{{ t('editor.email.blocks.image.name') }}
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<!-- Translation editor (editable) -->
|
|
48
|
+
<div class="flex-1 min-w-0">
|
|
49
|
+
<div class="flex items-center gap-2">
|
|
50
|
+
<MediaUploadOrSelect
|
|
51
|
+
v-model="targetBlock.settings.src"
|
|
52
|
+
media-type="image"
|
|
53
|
+
class="flex-1"
|
|
54
|
+
show-preview
|
|
55
|
+
:data-testid="emailTranslatorImageTestIds.mediaUpload"
|
|
56
|
+
>
|
|
57
|
+
<template #actions>
|
|
58
|
+
<UTooltip :text="t('editor.whatsapp.translator.reset_to_main')" v-if="isCustom">
|
|
59
|
+
<UButton
|
|
60
|
+
color="warning"
|
|
61
|
+
variant="ghost"
|
|
62
|
+
icon="ph:arrow-counter-clockwise-thin"
|
|
63
|
+
@click.stop="resetToMain"
|
|
64
|
+
:data-testid="emailTranslatorImageTestIds.resetButton"
|
|
65
|
+
/>
|
|
66
|
+
</UTooltip>
|
|
67
|
+
</template>
|
|
68
|
+
</MediaUploadOrSelect>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<!-- Link row -->
|
|
74
|
+
<div class="flex gap-8 items-start" v-if="baseBlock.settings.href">
|
|
75
|
+
<div class="flex-1 min-w-0">
|
|
76
|
+
<p class="paragraph-sm text-muted truncate">
|
|
77
|
+
{{ baseBlock.settings.href || '' }}
|
|
78
|
+
</p>
|
|
79
|
+
</div>
|
|
80
|
+
<div class="flex-1 min-w-0">
|
|
81
|
+
<UInput
|
|
82
|
+
v-model="targetBlock.settings.href"
|
|
83
|
+
type="url"
|
|
84
|
+
:placeholder="t('editor.email.blocks.image.settings.href_placeholder')"
|
|
85
|
+
size="sm"
|
|
86
|
+
:data-testid="emailTranslatorImageTestIds.hrefInput"
|
|
87
|
+
/>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
</template>
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, type Component } from 'vue'
|
|
3
|
+
import type {
|
|
4
|
+
EmailMessage,
|
|
5
|
+
EmailBlock,
|
|
6
|
+
PostEntity,
|
|
7
|
+
EmailBlockType,
|
|
8
|
+
} from '@dev.smartpricing/message-composer-utils/types'
|
|
9
|
+
import { getLanguageDisplayName } from '@dev.smartpricing/message-composer-utils/utils'
|
|
10
|
+
import { useI18n } from 'vue-i18n'
|
|
11
|
+
import HeadingTranslator from './Heading.vue'
|
|
12
|
+
import ParagraphTranslator from './Paragraph.vue'
|
|
13
|
+
import ButtonTranslator from './Button.vue'
|
|
14
|
+
import DiscountTranslator from './Discount.vue'
|
|
15
|
+
import FooterTranslator from './Footer.vue'
|
|
16
|
+
import RawHtmlTranslator from './RawHtml.vue'
|
|
17
|
+
import GridTranslator from './Grid.vue'
|
|
18
|
+
import UnsubscribeLinkTranslator from './UnsubscribeLink.vue'
|
|
19
|
+
import ImageTranslator from './Image.vue'
|
|
20
|
+
import HeaderTranslator from './Header.vue'
|
|
21
|
+
|
|
22
|
+
defineOptions({
|
|
23
|
+
name: 'EmailTranslator',
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const { baseMessage, baseLanguage, targetLanguage, filteredBlockIds } = defineProps<{
|
|
27
|
+
baseMessage: EmailMessage | PostEntity<EmailMessage>
|
|
28
|
+
baseLanguage: string
|
|
29
|
+
targetLanguage: string
|
|
30
|
+
filteredBlockIds?: string[]
|
|
31
|
+
}>()
|
|
32
|
+
|
|
33
|
+
const targetMessage = defineModel<EmailMessage | PostEntity<EmailMessage>>({
|
|
34
|
+
required: true,
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const { locale } = useI18n()
|
|
38
|
+
|
|
39
|
+
// Component registry
|
|
40
|
+
const translatorComponents: Partial<Record<EmailBlockType, Component>> = {
|
|
41
|
+
heading: HeadingTranslator,
|
|
42
|
+
paragraph: ParagraphTranslator,
|
|
43
|
+
button: ButtonTranslator,
|
|
44
|
+
discount: DiscountTranslator,
|
|
45
|
+
footer: FooterTranslator,
|
|
46
|
+
raw_html: RawHtmlTranslator,
|
|
47
|
+
grid: GridTranslator,
|
|
48
|
+
unsubscribe_link: UnsubscribeLinkTranslator,
|
|
49
|
+
image: ImageTranslator,
|
|
50
|
+
header: HeaderTranslator,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Get translatable blocks with indices
|
|
54
|
+
interface BlockInfo {
|
|
55
|
+
baseBlock: EmailBlock
|
|
56
|
+
targetBlock: EmailBlock
|
|
57
|
+
type: string
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const translatableBlocks = computed<BlockInfo[]>(() => {
|
|
61
|
+
// Build ID-based map of target blocks for quick lookup
|
|
62
|
+
const targetBlocksMap = new Map(targetMessage.value.body.blocks.map((b) => [b.id, b]))
|
|
63
|
+
|
|
64
|
+
return baseMessage.body.blocks
|
|
65
|
+
.map((block) => {
|
|
66
|
+
const targetBlock = targetBlocksMap.get(block.id)
|
|
67
|
+
if (!targetBlock) return
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
baseBlock: block,
|
|
71
|
+
targetBlock,
|
|
72
|
+
type: block.type,
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
.filter((block) => {
|
|
76
|
+
if (!block) return false
|
|
77
|
+
if (!translatorComponents[block.type]) return false
|
|
78
|
+
if (filteredBlockIds && !filteredBlockIds.includes(block.baseBlock.id)) return false
|
|
79
|
+
return true
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
// Get block type display name
|
|
84
|
+
function getBlockTypeLabel(type: string): string {
|
|
85
|
+
const t = useI18n().t
|
|
86
|
+
const key = `editor.email.blocks.${type}.name`
|
|
87
|
+
return t(key)
|
|
88
|
+
}
|
|
89
|
+
</script>
|
|
90
|
+
|
|
91
|
+
<template>
|
|
92
|
+
<div class="space-y-2" :data-testid="emailTranslatorIndexTestIds.root">
|
|
93
|
+
<!-- Translation cards for each block -->
|
|
94
|
+
<div
|
|
95
|
+
v-for="info in translatableBlocks"
|
|
96
|
+
:key="info.baseBlock.id"
|
|
97
|
+
class="bg-white border border-muted rounded-lg overflow-hidden"
|
|
98
|
+
:data-testid="`${emailTranslatorIndexTestIds.blockCard}-${info.baseBlock.id}`"
|
|
99
|
+
>
|
|
100
|
+
<!-- Card header with block title -->
|
|
101
|
+
<div class="px-4 py-3 border-b border-muted">
|
|
102
|
+
<h4 class="label-sm">
|
|
103
|
+
{{ getBlockTypeLabel(info.type) }}
|
|
104
|
+
</h4>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<!-- Card content -->
|
|
108
|
+
<div class="px-px">
|
|
109
|
+
<!-- Column headers -->
|
|
110
|
+
<div class="bg-muted px-4 py-2 flex gap-8">
|
|
111
|
+
<div class="flex-1 min-w-0">
|
|
112
|
+
<div class="flex items-center gap-1">
|
|
113
|
+
<p class="label-xs text-primary-700">
|
|
114
|
+
{{ getLanguageDisplayName(baseLanguage, locale) }}
|
|
115
|
+
</p>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
<div class="flex-1 min-w-0">
|
|
119
|
+
<div class="flex items-center gap-1">
|
|
120
|
+
<p class="label-xs text-primary-700">
|
|
121
|
+
{{ getLanguageDisplayName(targetLanguage, locale) }}
|
|
122
|
+
</p>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
<!-- Content row -->
|
|
128
|
+
<div class="px-4 py-3 show-list">
|
|
129
|
+
<component
|
|
130
|
+
:is="translatorComponents[info.type]"
|
|
131
|
+
:base-block="info.baseBlock"
|
|
132
|
+
v-model="info.targetBlock"
|
|
133
|
+
/>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
</template>
|
|
@@ -0,0 +1,66 @@
|
|
|
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 { ParagraphBlock } from '@dev.smartpricing/message-composer-utils/types'
|
|
8
|
+
|
|
9
|
+
defineOptions({
|
|
10
|
+
name: 'EmailTranslatorParagraph',
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
const { baseBlock } = defineProps<{
|
|
14
|
+
baseBlock: ParagraphBlock
|
|
15
|
+
}>()
|
|
16
|
+
|
|
17
|
+
const targetBlock = defineModel<ParagraphBlock>({
|
|
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> </p>',
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<template>
|
|
34
|
+
<div class="flex gap-8 items-start" :data-testid="emailTranslatorParagraphTestIds.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="emailTranslatorParagraphTestIds.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
|
+
italic
|
|
52
|
+
underline
|
|
53
|
+
lists
|
|
54
|
+
horizontal-rule
|
|
55
|
+
alignment
|
|
56
|
+
link
|
|
57
|
+
emoji
|
|
58
|
+
dynamic-values
|
|
59
|
+
:data-testid="emailTranslatorParagraphTestIds.contentEditor"
|
|
60
|
+
/>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<slot name="actions" />
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
</template>
|