@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,42 @@
|
|
|
1
|
+
import type { MaybeRefOrGetter } from 'vue'
|
|
2
|
+
import type { MediaType } from '@dev.smartpricing/message-composer-utils/types'
|
|
3
|
+
import { MEDIA_LIMITS } from '@dev.smartpricing/message-composer-utils/types'
|
|
4
|
+
|
|
5
|
+
export function useMediaLimits(mediaType: MaybeRefOrGetter<MediaType>) {
|
|
6
|
+
const { t } = useI18n()
|
|
7
|
+
|
|
8
|
+
const limits = computed(() => MEDIA_LIMITS[toValue(mediaType)])
|
|
9
|
+
|
|
10
|
+
const accept = computed(() => limits.value.accept.join(','))
|
|
11
|
+
|
|
12
|
+
const maxSize = computed(() => limits.value.maxSize)
|
|
13
|
+
|
|
14
|
+
const limitationsText = computed(() => {
|
|
15
|
+
const type = toValue(mediaType)
|
|
16
|
+
const sizeMB = limits.value.maxSize / (1024 * 1024)
|
|
17
|
+
return t(`common.imageUpload.mediaLimitations.${type}`, { maxSize: sizeMB })
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
function validateFile(file: File): boolean {
|
|
21
|
+
if (!limits.value.accept.includes(file.type)) {
|
|
22
|
+
useToast().add({
|
|
23
|
+
color: 'error',
|
|
24
|
+
title: t('common.imageUpload.mediaInvalidType'),
|
|
25
|
+
})
|
|
26
|
+
return false
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (file.size > limits.value.maxSize) {
|
|
30
|
+
const sizeMB = limits.value.maxSize / (1024 * 1024)
|
|
31
|
+
useToast().add({
|
|
32
|
+
color: 'error',
|
|
33
|
+
title: t('common.imageUpload.mediaFileTooLarge', { maxSize: sizeMB }),
|
|
34
|
+
})
|
|
35
|
+
return false
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return true
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return { accept, maxSize, limitationsText, validateFile }
|
|
42
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { computed, ref, type Ref, type ComputedRef } from 'vue'
|
|
3
|
+
import type {
|
|
4
|
+
EmailMessage,
|
|
5
|
+
MessageFromDb,
|
|
6
|
+
MessageGroupCategory,
|
|
7
|
+
PostEntity,
|
|
8
|
+
WhatsappMessage,
|
|
9
|
+
} from '@dev.smartpricing/message-composer-utils/types'
|
|
10
|
+
import { isEmailValid, isWhatsappValid } from '@dev.smartpricing/message-composer-utils/utils'
|
|
11
|
+
|
|
12
|
+
interface UseTemplateFormValidationOptions {
|
|
13
|
+
type: 'email' | 'whatsapp'
|
|
14
|
+
name: Ref<string> | ComputedRef<string>
|
|
15
|
+
category: Ref<MessageGroupCategory> | ComputedRef<MessageGroupCategory>
|
|
16
|
+
messages: Ref<Record<string, PostEntity<EmailMessage> | WhatsappMessage>>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const draftSchema = z.object({
|
|
20
|
+
name: z.string().min(1, 'editor.validation.error.nameRequired'),
|
|
21
|
+
category: z.string().min(1),
|
|
22
|
+
messages: z.record(z.string(), z.any()).refine((messages) => 'en' in messages, {
|
|
23
|
+
message: 'editor.validation.error.messageRequired',
|
|
24
|
+
}),
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
export function useTemplateFormValidation(options: UseTemplateFormValidationOptions) {
|
|
28
|
+
const { type, name, category, messages } = options
|
|
29
|
+
const { t } = useI18n()
|
|
30
|
+
const emailEditorStore = useEmailEditorStore()
|
|
31
|
+
|
|
32
|
+
const formRef = useTemplateRef('templateForm')
|
|
33
|
+
const validationErrors = ref<string[]>([])
|
|
34
|
+
|
|
35
|
+
const formState = computed(() => ({
|
|
36
|
+
name: name.value,
|
|
37
|
+
category: category.value,
|
|
38
|
+
messages: messages.value,
|
|
39
|
+
}))
|
|
40
|
+
|
|
41
|
+
function clearErrors() {
|
|
42
|
+
validationErrors.value = []
|
|
43
|
+
formRef.value?.clear()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function validateDraft(): Promise<boolean> {
|
|
47
|
+
clearErrors()
|
|
48
|
+
try {
|
|
49
|
+
await formRef.value?.validate()
|
|
50
|
+
return true
|
|
51
|
+
} catch {
|
|
52
|
+
const formErrors = formRef.value?.errors
|
|
53
|
+
const errors: string[] = []
|
|
54
|
+
if (Array.isArray(formErrors)) {
|
|
55
|
+
for (const err of formErrors) {
|
|
56
|
+
if (err.message) {
|
|
57
|
+
errors.push(t(err.message))
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
validationErrors.value = errors
|
|
62
|
+
return false
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function validateFull(): Promise<boolean> {
|
|
67
|
+
const draftValid = await validateDraft()
|
|
68
|
+
if (!draftValid) return false
|
|
69
|
+
|
|
70
|
+
const errors: string[] = []
|
|
71
|
+
const messageEntries = Object.entries(messages.value)
|
|
72
|
+
const hasMultipleLanguages = messageEntries.length > 1
|
|
73
|
+
for (const [langId, message] of messageEntries) {
|
|
74
|
+
const result =
|
|
75
|
+
type === 'email'
|
|
76
|
+
? isEmailValid(message as unknown as EmailMessage, emailEditorStore.brand)
|
|
77
|
+
: isWhatsappValid(message as unknown as MessageFromDb)
|
|
78
|
+
if (!result.valid) {
|
|
79
|
+
const uniqueErrors = [...new Set(result.errors)]
|
|
80
|
+
for (const error of uniqueErrors) {
|
|
81
|
+
const translated = t(error)
|
|
82
|
+
if (hasMultipleLanguages) {
|
|
83
|
+
errors.push(`${langId.toUpperCase()}: ${translated}`)
|
|
84
|
+
} else {
|
|
85
|
+
errors.push(translated)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (errors.length > 0) {
|
|
92
|
+
validationErrors.value = errors
|
|
93
|
+
formRef.value?.setErrors(errors.map((message) => ({ message, name: 'messages' })))
|
|
94
|
+
return false
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return true
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
formRef,
|
|
102
|
+
formState,
|
|
103
|
+
draftSchema,
|
|
104
|
+
validationErrors,
|
|
105
|
+
validateDraft,
|
|
106
|
+
validateFull,
|
|
107
|
+
clearErrors,
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { ref } from 'vue'
|
|
2
|
+
import {
|
|
3
|
+
useCreateMessageGroupMutation,
|
|
4
|
+
useUpdateMessageGroupMutation,
|
|
5
|
+
useCreateMessageMutation,
|
|
6
|
+
useUpdateMessageMutation,
|
|
7
|
+
} from '~/queries'
|
|
8
|
+
import { messageGroupsApi } from '~/api'
|
|
9
|
+
import type {
|
|
10
|
+
MessageFromDb,
|
|
11
|
+
MessageGroupCategory,
|
|
12
|
+
Message,
|
|
13
|
+
PostEntity,
|
|
14
|
+
MessageStatus,
|
|
15
|
+
} from '@dev.smartpricing/message-composer-utils/types'
|
|
16
|
+
|
|
17
|
+
interface TemplateData<T = Message | PostEntity<Message>> {
|
|
18
|
+
messageGroup: { name: string; category: MessageGroupCategory; brand_id?: string | null }
|
|
19
|
+
messages: Record<string, T>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface UseTemplateOperationsOptions {
|
|
23
|
+
onSuccess?: () => void
|
|
24
|
+
onError?: (error: Error) => void
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function transformMessagesForCreation(
|
|
28
|
+
messages: MessageFromDb[],
|
|
29
|
+
): Record<string, PostEntity<Message>> {
|
|
30
|
+
return messages.reduce(
|
|
31
|
+
(acc, message) => {
|
|
32
|
+
// Remove DB-specific fields, keep message group content
|
|
33
|
+
|
|
34
|
+
const { id, message_group_id, created_at, updated_at, ...messageGroupMessage } = message
|
|
35
|
+
acc[message.language_id] = { ...messageGroupMessage, message_group_id: '', metadata: {} }
|
|
36
|
+
|
|
37
|
+
return acc
|
|
38
|
+
},
|
|
39
|
+
{} as Record<string, PostEntity<Message>>,
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function useTemplateOperations(options: UseTemplateOperationsOptions = {}) {
|
|
44
|
+
const { t } = useI18n()
|
|
45
|
+
const saving = ref(false)
|
|
46
|
+
const saved = ref(false)
|
|
47
|
+
|
|
48
|
+
// Hoist composables to setup scope (they use inject() internally)
|
|
49
|
+
const { mutateAsync: createMessageGroup } = useCreateMessageGroupMutation()
|
|
50
|
+
const { mutateAsync: createMessage } = useCreateMessageMutation()
|
|
51
|
+
const { mutateAsync: updateMessageGroup } = useUpdateMessageGroupMutation()
|
|
52
|
+
const { mutateAsync: updateMessage } = useUpdateMessageMutation()
|
|
53
|
+
|
|
54
|
+
// Create new message group(used by Create + Duplicate)
|
|
55
|
+
async function createTemplate(data: TemplateData, status: MessageStatus = 'draft') {
|
|
56
|
+
saving.value = true
|
|
57
|
+
try {
|
|
58
|
+
const createdMessageGroup = await createMessageGroup(data.messageGroup)
|
|
59
|
+
|
|
60
|
+
if (createdMessageGroup?.id) {
|
|
61
|
+
for (const message of Object.values(data.messages)) {
|
|
62
|
+
message.message_group_id = createdMessageGroup.id
|
|
63
|
+
message.status = status
|
|
64
|
+
|
|
65
|
+
await createMessage(message)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
saved.value = true
|
|
70
|
+
options.onSuccess?.()
|
|
71
|
+
return createdMessageGroup
|
|
72
|
+
} catch (error) {
|
|
73
|
+
options.onError?.(error as Error)
|
|
74
|
+
throw error
|
|
75
|
+
} finally {
|
|
76
|
+
saving.value = false
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Update existing message group (used by Edit)
|
|
81
|
+
async function updateTemplate(
|
|
82
|
+
id: string,
|
|
83
|
+
data: TemplateData<Message | PostEntity<Message>>,
|
|
84
|
+
status?: MessageStatus,
|
|
85
|
+
) {
|
|
86
|
+
saving.value = true
|
|
87
|
+
try {
|
|
88
|
+
await updateMessageGroup({ id, data: data.messageGroup })
|
|
89
|
+
|
|
90
|
+
for (const message of Object.values(data.messages)) {
|
|
91
|
+
if (status) {
|
|
92
|
+
message.status = status
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if ('id' in message) {
|
|
96
|
+
await updateMessage({ id: message.id, data: message })
|
|
97
|
+
} else {
|
|
98
|
+
message.message_group_id = id
|
|
99
|
+
await createMessage(message)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
saved.value = true
|
|
104
|
+
options.onSuccess?.()
|
|
105
|
+
} catch (error) {
|
|
106
|
+
options.onError?.(error as Error)
|
|
107
|
+
throw error
|
|
108
|
+
} finally {
|
|
109
|
+
saving.value = false
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Duplicate existing message group (used by Duplicate action)
|
|
114
|
+
async function duplicateTemplate(sourceId: string, newName?: string) {
|
|
115
|
+
saving.value = true
|
|
116
|
+
try {
|
|
117
|
+
const sourceTemplate = await messageGroupsApi.getById(sourceId)
|
|
118
|
+
|
|
119
|
+
if (!sourceTemplate) {
|
|
120
|
+
throw new Error('Source message group not found')
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const templateData: TemplateData = {
|
|
124
|
+
messageGroup: {
|
|
125
|
+
name: newName || t('common.copy_of', { name: sourceTemplate.name }),
|
|
126
|
+
category: sourceTemplate.category,
|
|
127
|
+
brand_id: sourceTemplate.brand_id,
|
|
128
|
+
},
|
|
129
|
+
messages: transformMessagesForCreation(sourceTemplate.messages),
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const result = await createTemplate(templateData, 'draft')
|
|
133
|
+
return result
|
|
134
|
+
} catch (error) {
|
|
135
|
+
options.onError?.(error as Error)
|
|
136
|
+
throw error
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
saving,
|
|
142
|
+
saved,
|
|
143
|
+
createTemplate,
|
|
144
|
+
updateTemplate,
|
|
145
|
+
duplicateTemplate,
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { TrackingEventMap } from '~/types/tracking'
|
|
2
|
+
|
|
3
|
+
export function useTracking() {
|
|
4
|
+
const { product, mainLanguage } = storeToRefs(useAppContextStore())
|
|
5
|
+
const { isAdminModeActive } = storeToRefs(useAdminStore())
|
|
6
|
+
|
|
7
|
+
function getDefaultProperties(): Record<string, unknown> {
|
|
8
|
+
return {
|
|
9
|
+
event_origin: 'message-composer',
|
|
10
|
+
product: product.value,
|
|
11
|
+
main_language: mainLanguage.value,
|
|
12
|
+
is_admin: isAdminModeActive.value,
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function trackEvent<T extends keyof TrackingEventMap>(
|
|
17
|
+
eventName: T,
|
|
18
|
+
properties?: TrackingEventMap[T],
|
|
19
|
+
): Promise<void> {
|
|
20
|
+
if (!useAppConfig().composer?.tracking) return
|
|
21
|
+
try {
|
|
22
|
+
const { trackingApi } = await import('@/api/tracking')
|
|
23
|
+
await trackingApi.event(eventName, {
|
|
24
|
+
...getDefaultProperties(),
|
|
25
|
+
...properties,
|
|
26
|
+
})
|
|
27
|
+
} catch {
|
|
28
|
+
if (import.meta.dev) {
|
|
29
|
+
console.warn(`[Tracking] Failed to track "${eventName}"`, properties)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function trackPageView(pageName: string): void {
|
|
35
|
+
trackEvent('Page View', { page_name: pageName })
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return { trackEvent, trackPageView }
|
|
39
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { computed, type Ref, type ComputedRef } from 'vue'
|
|
2
|
+
import type { WhatsappMessage, PostEntity } from '@dev.smartpricing/message-composer-utils/types'
|
|
3
|
+
import { extractText, isTiptapEmpty } from '@dev.smartpricing/message-composer-utils/utils'
|
|
4
|
+
import { useChangeTracking } from './useChangeTracking'
|
|
5
|
+
|
|
6
|
+
type Section = 'header' | 'body' | 'footer' | 'buttons'
|
|
7
|
+
|
|
8
|
+
interface TranslationFiltersParams {
|
|
9
|
+
messages: Ref<Record<string, PostEntity<WhatsappMessage>>>
|
|
10
|
+
currentLanguage: Ref<string>
|
|
11
|
+
baseLanguage: Ref<string>
|
|
12
|
+
messageGroupId?: Ref<string | number | undefined>
|
|
13
|
+
searchQuery: Ref<string>
|
|
14
|
+
showOnlyMissing: Ref<boolean>
|
|
15
|
+
showOnlyChanges: Ref<boolean>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function useTranslationFilters(params: TranslationFiltersParams) {
|
|
19
|
+
const {
|
|
20
|
+
messages,
|
|
21
|
+
currentLanguage,
|
|
22
|
+
baseLanguage,
|
|
23
|
+
messageGroupId,
|
|
24
|
+
searchQuery,
|
|
25
|
+
showOnlyMissing,
|
|
26
|
+
showOnlyChanges,
|
|
27
|
+
} = params
|
|
28
|
+
|
|
29
|
+
// Initialize change tracking if messageGroupId is provided
|
|
30
|
+
const changeTracking = computed(() => {
|
|
31
|
+
if (!messageGroupId?.value) return null
|
|
32
|
+
return useChangeTracking(messageGroupId.value)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Check if a section matches the search query
|
|
37
|
+
*/
|
|
38
|
+
function matchesSearch(section: Section, baseMsg: PostEntity<WhatsappMessage>): boolean {
|
|
39
|
+
if (!searchQuery.value || !searchQuery.value.trim()) return true
|
|
40
|
+
|
|
41
|
+
const query = searchQuery.value.toLowerCase().trim()
|
|
42
|
+
let searchText = ''
|
|
43
|
+
|
|
44
|
+
switch (section) {
|
|
45
|
+
case 'header':
|
|
46
|
+
if (baseMsg.body?.header?.type === 'text' && baseMsg.body.header.content) {
|
|
47
|
+
searchText = extractText(baseMsg.body.header.content, baseMsg.render_type)
|
|
48
|
+
}
|
|
49
|
+
break
|
|
50
|
+
case 'body':
|
|
51
|
+
if (baseMsg.body?.body?.content) {
|
|
52
|
+
searchText = extractText(baseMsg.body.body.content, baseMsg.render_type)
|
|
53
|
+
}
|
|
54
|
+
break
|
|
55
|
+
case 'footer':
|
|
56
|
+
if (baseMsg.body?.footer?.content) {
|
|
57
|
+
searchText = extractText(baseMsg.body.footer.content, baseMsg.render_type)
|
|
58
|
+
}
|
|
59
|
+
break
|
|
60
|
+
case 'buttons':
|
|
61
|
+
if (baseMsg.body?.buttons?.buttons) {
|
|
62
|
+
searchText = baseMsg.body.buttons.buttons
|
|
63
|
+
.map((btn) => extractText(btn?.text || '', baseMsg.render_type))
|
|
64
|
+
.join(' ')
|
|
65
|
+
}
|
|
66
|
+
break
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return searchText.toLowerCase().includes(query)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Check if a section has missing translations
|
|
74
|
+
*/
|
|
75
|
+
function hasMissingTranslation(
|
|
76
|
+
section: Section,
|
|
77
|
+
targetMsg: PostEntity<WhatsappMessage>,
|
|
78
|
+
): boolean {
|
|
79
|
+
if (!targetMsg.body) return true
|
|
80
|
+
|
|
81
|
+
switch (section) {
|
|
82
|
+
case 'header':
|
|
83
|
+
// Header is optional, only check if it exists in base
|
|
84
|
+
if (targetMsg.body.header?.type === 'text') {
|
|
85
|
+
return isTiptapEmpty(targetMsg.body.header.content, targetMsg.render_type)
|
|
86
|
+
}
|
|
87
|
+
if (['image', 'video', 'document'].includes(targetMsg.body.header?.type)) {
|
|
88
|
+
return !('url' in targetMsg.body.header) || !targetMsg.body.header.url
|
|
89
|
+
}
|
|
90
|
+
return false
|
|
91
|
+
|
|
92
|
+
case 'body':
|
|
93
|
+
// Body is required
|
|
94
|
+
return isTiptapEmpty(targetMsg.body.body?.content, targetMsg.render_type)
|
|
95
|
+
|
|
96
|
+
case 'footer':
|
|
97
|
+
// Footer is optional
|
|
98
|
+
return isTiptapEmpty(targetMsg.body.footer?.content, targetMsg.render_type)
|
|
99
|
+
|
|
100
|
+
case 'buttons':
|
|
101
|
+
// Check if any button has empty text
|
|
102
|
+
if (targetMsg.body.buttons?.buttons) {
|
|
103
|
+
return targetMsg.body.buttons.buttons.some((btn) =>
|
|
104
|
+
isTiptapEmpty(btn?.text, targetMsg.render_type),
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
return false
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Check if a section has changes
|
|
113
|
+
*/
|
|
114
|
+
function hasChanges(section: Section): boolean {
|
|
115
|
+
const tracking = changeTracking.value
|
|
116
|
+
if (!tracking || !currentLanguage.value) return false
|
|
117
|
+
|
|
118
|
+
const currentMsg = messages.value[currentLanguage.value]
|
|
119
|
+
if (!currentMsg) return false
|
|
120
|
+
|
|
121
|
+
const changedFields = tracking.getChangedFields(
|
|
122
|
+
currentLanguage.value,
|
|
123
|
+
currentMsg as WhatsappMessage,
|
|
124
|
+
)
|
|
125
|
+
return changedFields.includes(section)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Filter sections based on active filters
|
|
130
|
+
*/
|
|
131
|
+
const filteredSections: ComputedRef<Section[]> = computed(() => {
|
|
132
|
+
const baseMsg = messages.value[baseLanguage.value]
|
|
133
|
+
const targetMsg = messages.value[currentLanguage.value]
|
|
134
|
+
|
|
135
|
+
if (!baseMsg || !targetMsg) {
|
|
136
|
+
return []
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const allSections: Section[] = ['header', 'body', 'footer', 'buttons']
|
|
140
|
+
|
|
141
|
+
return allSections.filter((section) => {
|
|
142
|
+
// Search filter
|
|
143
|
+
if (searchQuery.value && !matchesSearch(section, baseMsg)) {
|
|
144
|
+
return false
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Missing translations filter
|
|
148
|
+
if (showOnlyMissing.value && !hasMissingTranslation(section, targetMsg)) {
|
|
149
|
+
return false
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Changes filter
|
|
153
|
+
if (showOnlyChanges.value && !hasChanges(section)) {
|
|
154
|
+
return false
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return true
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Check if any filters are active
|
|
163
|
+
*/
|
|
164
|
+
const hasActiveFilters: ComputedRef<boolean> = computed(() => {
|
|
165
|
+
return (
|
|
166
|
+
(searchQuery.value && searchQuery.value.trim() !== '') ||
|
|
167
|
+
showOnlyMissing.value ||
|
|
168
|
+
showOnlyChanges.value
|
|
169
|
+
)
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
filteredSections,
|
|
174
|
+
hasActiveFilters,
|
|
175
|
+
}
|
|
176
|
+
}
|