@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,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>
|