@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,112 @@
|
|
|
1
|
+
import { useQuery, useMutation, useQueryCache } from '@pinia/colada'
|
|
2
|
+
import type {
|
|
3
|
+
MessageGroup,
|
|
4
|
+
MessageGroupWithMessageSummary,
|
|
5
|
+
PostEntity,
|
|
6
|
+
RenderType,
|
|
7
|
+
} from '@dev.smartpricing/message-composer-utils/types'
|
|
8
|
+
import { messageGroupsApi } from '~/api'
|
|
9
|
+
|
|
10
|
+
// QUERIES
|
|
11
|
+
export function useMessageGroupsQuery(
|
|
12
|
+
options?: MaybeRefOrGetter<{
|
|
13
|
+
renderType?: RenderType
|
|
14
|
+
tags?: string[]
|
|
15
|
+
tagsMode?: 'and' | 'or'
|
|
16
|
+
}>,
|
|
17
|
+
_queryOptions?: { polling?: boolean },
|
|
18
|
+
) {
|
|
19
|
+
const query = useQuery({
|
|
20
|
+
key: () => ['message-groups', JSON.stringify(toValue(options))],
|
|
21
|
+
query: () => messageGroupsApi.getAll(toValue(options)),
|
|
22
|
+
staleTime: 0,
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
// TODO: Implement polling when Pinia Colada supports it
|
|
26
|
+
// For now, polling can be done manually in the component using setInterval + refetch
|
|
27
|
+
|
|
28
|
+
return query
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function useMessageGroupQuery(id: MaybeRefOrGetter<string>) {
|
|
32
|
+
return useQuery({
|
|
33
|
+
key: () => ['message-groups', toValue(id)],
|
|
34
|
+
query: () => messageGroupsApi.getById(toValue(id)),
|
|
35
|
+
staleTime: 2 * 60 * 1000,
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// MUTATIONS
|
|
40
|
+
export function useCreateMessageGroupMutation() {
|
|
41
|
+
const queryCache = useQueryCache()
|
|
42
|
+
|
|
43
|
+
return useMutation({
|
|
44
|
+
mutation: messageGroupsApi.create,
|
|
45
|
+
onSuccess: () => {
|
|
46
|
+
queryCache.invalidateQueries({ key: ['message-groups'] })
|
|
47
|
+
},
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function useUpdateMessageGroupMutation() {
|
|
52
|
+
const queryCache = useQueryCache()
|
|
53
|
+
|
|
54
|
+
return useMutation({
|
|
55
|
+
mutation: ({ id, data }: { id: string; data: Partial<PostEntity<MessageGroup>> }) =>
|
|
56
|
+
messageGroupsApi.update(id, data),
|
|
57
|
+
onSuccess: (_, { id }) => {
|
|
58
|
+
queryCache.invalidateQueries({ key: ['message-groups', id] })
|
|
59
|
+
},
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function usePublishMessageGroupMutation() {
|
|
64
|
+
const queryCache = useQueryCache()
|
|
65
|
+
|
|
66
|
+
return useMutation({
|
|
67
|
+
mutation: messageGroupsApi.publish,
|
|
68
|
+
onSuccess: () => {
|
|
69
|
+
queryCache.invalidateQueries({ key: ['message-groups'] })
|
|
70
|
+
},
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function useUnpublishMessageGroupMutation() {
|
|
75
|
+
const queryCache = useQueryCache()
|
|
76
|
+
|
|
77
|
+
return useMutation({
|
|
78
|
+
mutation: messageGroupsApi.unpublish,
|
|
79
|
+
onSuccess: () => {
|
|
80
|
+
queryCache.invalidateQueries({ key: ['message-groups'] })
|
|
81
|
+
},
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function useDeleteMessageGroupMutation() {
|
|
86
|
+
const queryCache = useQueryCache()
|
|
87
|
+
|
|
88
|
+
return useMutation({
|
|
89
|
+
mutation: messageGroupsApi.delete,
|
|
90
|
+
onMutate: async (id) => {
|
|
91
|
+
// Optimistic update
|
|
92
|
+
queryCache.cancelQueries({ key: ['message-groups'] })
|
|
93
|
+
const previous = queryCache.getQueryData(['message-groups'])
|
|
94
|
+
|
|
95
|
+
queryCache.setQueryData(
|
|
96
|
+
['message-groups'],
|
|
97
|
+
(old: MessageGroupWithMessageSummary[] | undefined) => old?.filter((g) => g.id !== id),
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
return { previous }
|
|
101
|
+
},
|
|
102
|
+
onError: (_, __, context) => {
|
|
103
|
+
// Rollback on error
|
|
104
|
+
if (context?.previous) {
|
|
105
|
+
queryCache.setQueryData(['message-groups'], context.previous)
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
onSuccess: () => {
|
|
109
|
+
queryCache.invalidateQueries({ key: ['message-groups'] })
|
|
110
|
+
},
|
|
111
|
+
})
|
|
112
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { useMutation, useQueryCache } from '@pinia/colada'
|
|
2
|
+
import type { Message, PostEntity } from '@dev.smartpricing/message-composer-utils/types'
|
|
3
|
+
import { messagesApi } from '~/api'
|
|
4
|
+
|
|
5
|
+
export function useCreateMessageMutation() {
|
|
6
|
+
const queryCache = useQueryCache()
|
|
7
|
+
|
|
8
|
+
return useMutation({
|
|
9
|
+
mutation: messagesApi.create,
|
|
10
|
+
onSuccess: (data) => {
|
|
11
|
+
queryCache.invalidateQueries({
|
|
12
|
+
key: ['message-groups', data.message_group_id],
|
|
13
|
+
})
|
|
14
|
+
},
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function useUpdateMessageMutation() {
|
|
19
|
+
const queryCache = useQueryCache()
|
|
20
|
+
|
|
21
|
+
return useMutation({
|
|
22
|
+
mutation: ({ id, data }: { id: string; data: Partial<PostEntity<Message>> }) =>
|
|
23
|
+
messagesApi.update(id, data),
|
|
24
|
+
onSuccess: (data) => {
|
|
25
|
+
queryCache.invalidateQueries({
|
|
26
|
+
key: ['message-groups', data.message_group_id],
|
|
27
|
+
})
|
|
28
|
+
},
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function useDeleteMessageMutation() {
|
|
33
|
+
const queryCache = useQueryCache()
|
|
34
|
+
|
|
35
|
+
return useMutation({
|
|
36
|
+
mutation: ({ id, messageGroupId }: { id: string; messageGroupId: string }) =>
|
|
37
|
+
messagesApi.delete(id).then(() => messageGroupId),
|
|
38
|
+
onSuccess: (messageGroupId) => {
|
|
39
|
+
queryCache.invalidateQueries({ key: ['message-groups', messageGroupId] })
|
|
40
|
+
},
|
|
41
|
+
})
|
|
42
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { useMutation, useQueryCache } from '@pinia/colada'
|
|
2
|
+
import { metaApi } from '~/api'
|
|
3
|
+
|
|
4
|
+
export function useSubmitToMetaMutation() {
|
|
5
|
+
const queryCache = useQueryCache()
|
|
6
|
+
|
|
7
|
+
return useMutation({
|
|
8
|
+
mutation: metaApi.submitGroup,
|
|
9
|
+
onSuccess: (_, messageGroupId) => {
|
|
10
|
+
queryCache.invalidateQueries({ key: ['message-groups', messageGroupId] })
|
|
11
|
+
},
|
|
12
|
+
})
|
|
13
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { jwtDecode } from 'jwt-decode'
|
|
2
|
+
|
|
3
|
+
interface TokenData {
|
|
4
|
+
id: number
|
|
5
|
+
role: string
|
|
6
|
+
accounting: boolean
|
|
7
|
+
email: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const useAdminStore = defineStore('admin', () => {
|
|
11
|
+
const appContext = useAppContextStore()
|
|
12
|
+
const adminModeActive = ref(false)
|
|
13
|
+
|
|
14
|
+
const isAccounting = computed(() => {
|
|
15
|
+
const token = appContext.authenticationToken
|
|
16
|
+
if (!token) return false
|
|
17
|
+
try {
|
|
18
|
+
const payload = jwtDecode<TokenData>(token)
|
|
19
|
+
return payload.accounting === true
|
|
20
|
+
} catch {
|
|
21
|
+
return false
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
function activateAdminMode() {
|
|
26
|
+
if (!isAccounting.value) return
|
|
27
|
+
adminModeActive.value = true
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function deactivateAdminMode() {
|
|
31
|
+
adminModeActive.value = false
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function toggleAdminMode() {
|
|
35
|
+
if (!isAccounting.value) {
|
|
36
|
+
adminModeActive.value = false
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
adminModeActive.value = !adminModeActive.value
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const isAdminModeActive = computed(() => adminModeActive.value)
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
activateAdminMode,
|
|
46
|
+
deactivateAdminMode,
|
|
47
|
+
isAdminModeActive,
|
|
48
|
+
toggleAdminMode,
|
|
49
|
+
}
|
|
50
|
+
})
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { ref, computed, watch } from 'vue'
|
|
2
|
+
import type {
|
|
3
|
+
AppContextConfig,
|
|
4
|
+
AppContextState,
|
|
5
|
+
} from '@dev.smartpricing/message-composer-utils/types'
|
|
6
|
+
|
|
7
|
+
export const useAppContextStore = defineStore('appContext', () => {
|
|
8
|
+
const ctx = useComposerContext()
|
|
9
|
+
|
|
10
|
+
const isInitialized = ref<boolean>(false)
|
|
11
|
+
|
|
12
|
+
// Proxy from injection with defaults (same interface as before)
|
|
13
|
+
const authenticationToken = computed(() => ctx.authenticationToken.value)
|
|
14
|
+
const owner_id = computed(() => ctx.owner_id?.value ?? '')
|
|
15
|
+
const product = computed<NonNullable<AppContextConfig['product']>>(
|
|
16
|
+
() => ctx.product?.value ?? 'browser',
|
|
17
|
+
)
|
|
18
|
+
const strategy = computed<NonNullable<AppContextConfig['strategy']>>(
|
|
19
|
+
() => ctx.strategy?.value ?? 'native',
|
|
20
|
+
)
|
|
21
|
+
const channels = computed<NonNullable<AppContextConfig['channels']>>(
|
|
22
|
+
() => ctx.channels?.value ?? ['email', 'whatsapp'],
|
|
23
|
+
)
|
|
24
|
+
const languages = computed<string[]>(() => ctx.languages?.value ?? ['en', 'de', 'fr', 'it', 'es'])
|
|
25
|
+
const mainLanguage = computed(() => ctx.mainLanguage?.value ?? 'en')
|
|
26
|
+
const dinamicValues = computed<Array<{ value: string; label: string }>>(
|
|
27
|
+
() => ctx.dinamicValues?.value ?? [],
|
|
28
|
+
)
|
|
29
|
+
const default_tags = computed<string[]>(() => ctx.default_tags?.value ?? [])
|
|
30
|
+
const metaWabaId = computed(() => ctx.metaWabaId?.value ?? '')
|
|
31
|
+
const metaAccessToken = computed(() => ctx.metaAccessToken?.value ?? '')
|
|
32
|
+
|
|
33
|
+
// Sync i18n locale when mainLanguage changes
|
|
34
|
+
watch(
|
|
35
|
+
mainLanguage,
|
|
36
|
+
(value) => {
|
|
37
|
+
if (isLocaleManaged(value)) {
|
|
38
|
+
useNuxtApp().$i18n.setLocale(value)
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
{ immediate: true },
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
// Computed context object for convenience
|
|
45
|
+
const context = computed<AppContextState>(() => ({
|
|
46
|
+
authenticationToken: authenticationToken.value,
|
|
47
|
+
product: product.value,
|
|
48
|
+
strategy: strategy.value,
|
|
49
|
+
channels: channels.value,
|
|
50
|
+
languages: languages.value,
|
|
51
|
+
mainLanguage: mainLanguage.value,
|
|
52
|
+
metaWabaId: metaWabaId.value,
|
|
53
|
+
metaAccessToken: metaAccessToken.value,
|
|
54
|
+
owner_id: owner_id.value,
|
|
55
|
+
default_tags: default_tags.value,
|
|
56
|
+
initializationSource: 'injection',
|
|
57
|
+
}))
|
|
58
|
+
|
|
59
|
+
// Validation
|
|
60
|
+
const validationErrors = computed<Record<string, string>>(() => {
|
|
61
|
+
const errors: Record<string, string> = {}
|
|
62
|
+
|
|
63
|
+
if (!authenticationToken.value) {
|
|
64
|
+
errors.authenticationToken = 'Authentication token is required'
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (channels.value?.includes('whatsapp') && !metaWabaId.value) {
|
|
68
|
+
errors.metaConfig = 'Meta WABA ID is required for WhatsApp'
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return errors
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
// Initialize — parent is responsible for providing valid context
|
|
75
|
+
async function initialize() {
|
|
76
|
+
isInitialized.value = true
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
authenticationToken,
|
|
81
|
+
product,
|
|
82
|
+
strategy,
|
|
83
|
+
channels,
|
|
84
|
+
languages,
|
|
85
|
+
mainLanguage,
|
|
86
|
+
dinamicValues,
|
|
87
|
+
metaWabaId,
|
|
88
|
+
metaAccessToken,
|
|
89
|
+
owner_id,
|
|
90
|
+
default_tags,
|
|
91
|
+
|
|
92
|
+
isInitialized: computed(() => isInitialized.value),
|
|
93
|
+
|
|
94
|
+
context,
|
|
95
|
+
validationErrors,
|
|
96
|
+
|
|
97
|
+
initialize,
|
|
98
|
+
}
|
|
99
|
+
})
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { ref, computed } from 'vue'
|
|
2
|
+
import {
|
|
3
|
+
type EmailBlock,
|
|
4
|
+
type EmailGlobalSettings,
|
|
5
|
+
type PostEntity,
|
|
6
|
+
type EmailMessage,
|
|
7
|
+
type BrandFromDb,
|
|
8
|
+
} from '@dev.smartpricing/message-composer-utils/types'
|
|
9
|
+
import { resolveEmailSettings } from '@dev.smartpricing/message-composer-utils/utils'
|
|
10
|
+
import { applySocialDefaults } from '~/utils/socialDefaults'
|
|
11
|
+
import { brandsApi } from '~/api'
|
|
12
|
+
|
|
13
|
+
export const useEmailEditorStore = defineStore('emailEditor', () => {
|
|
14
|
+
// State
|
|
15
|
+
const blocks = ref<EmailBlock[]>([])
|
|
16
|
+
const selectedBlockId = ref<string | null>(null)
|
|
17
|
+
const globalSettings = ref<EmailGlobalSettings>({})
|
|
18
|
+
const brands = ref<BrandFromDb[]>([])
|
|
19
|
+
const brand = ref<BrandFromDb | null>(null)
|
|
20
|
+
|
|
21
|
+
// Brand computed helpers
|
|
22
|
+
const brandId = computed(() => brand.value?.id ?? null)
|
|
23
|
+
const brandColors = computed(() => brand.value?.colors ?? [])
|
|
24
|
+
|
|
25
|
+
// Resolved settings: globalSettings → brand defaults → hardcoded defaults
|
|
26
|
+
const resolvedSettings = computed(() => resolveEmailSettings(globalSettings.value, brand.value))
|
|
27
|
+
|
|
28
|
+
// Helper to find block recursively
|
|
29
|
+
function findBlockRecursively(
|
|
30
|
+
blockId: string,
|
|
31
|
+
searchBlocks: EmailBlock[],
|
|
32
|
+
): EmailBlock | undefined {
|
|
33
|
+
for (const block of searchBlocks) {
|
|
34
|
+
if (block.id === blockId) return block
|
|
35
|
+
|
|
36
|
+
// Search in grid columns
|
|
37
|
+
if (block.type === 'grid' && block.settings.columnData) {
|
|
38
|
+
for (const column of block.settings.columnData) {
|
|
39
|
+
const found = findBlockRecursively(blockId, column.blocks)
|
|
40
|
+
if (found) return found
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return undefined
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Computed
|
|
48
|
+
const selectedBlock = computed(() => {
|
|
49
|
+
if (!selectedBlockId.value) return undefined
|
|
50
|
+
return findBlockRecursively(selectedBlockId.value, blocks.value)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const hasBlocks = computed(() => blocks.value.length > 0)
|
|
54
|
+
|
|
55
|
+
// Actions - Block CRUD
|
|
56
|
+
function addBlock(block: EmailBlock) {
|
|
57
|
+
const newBlock = {
|
|
58
|
+
...structuredClone(toRaw(block)),
|
|
59
|
+
id: crypto.randomUUID(),
|
|
60
|
+
}
|
|
61
|
+
// Auto-enable socials that have brand URLs
|
|
62
|
+
if (newBlock.type === 'socials' && brand.value?.default_socials) {
|
|
63
|
+
newBlock.settings.socials = applySocialDefaults(brand.value.default_socials)
|
|
64
|
+
}
|
|
65
|
+
blocks.value.push(newBlock)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function insertBlock(block: EmailBlock, index: number) {
|
|
69
|
+
blocks.value.splice(index, 0, block)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function removeBlock(blockId: string) {
|
|
73
|
+
const index = blocks.value.findIndex((b) => b.id === blockId)
|
|
74
|
+
if (index > -1) {
|
|
75
|
+
blocks.value.splice(index, 1)
|
|
76
|
+
// Deselect if removing selected block
|
|
77
|
+
if (selectedBlockId.value === blockId) {
|
|
78
|
+
selectedBlockId.value = null
|
|
79
|
+
}
|
|
80
|
+
// Reorder remaining blocks
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function updateBlock(blockId: string, updates: Partial<EmailBlock>) {
|
|
85
|
+
const block = findBlockRecursively(blockId, blocks.value)
|
|
86
|
+
if (block) {
|
|
87
|
+
Object.assign(block, updates)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function duplicateBlock(blockId: string) {
|
|
92
|
+
const index = blocks.value.findIndex((b) => b.id === blockId)
|
|
93
|
+
const block = blocks.value[index]
|
|
94
|
+
if (block) {
|
|
95
|
+
const newBlock = {
|
|
96
|
+
...structuredClone(toRaw(block)),
|
|
97
|
+
id: crypto.randomUUID(),
|
|
98
|
+
}
|
|
99
|
+
insertBlock(newBlock, index + 1)
|
|
100
|
+
return newBlock.id
|
|
101
|
+
}
|
|
102
|
+
return null
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Actions - Selection
|
|
106
|
+
function selectBlock(blockId: string | null) {
|
|
107
|
+
selectedBlockId.value = blockId
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function clearSelection() {
|
|
111
|
+
selectedBlockId.value = null
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Actions - Global Settings
|
|
115
|
+
function updateGlobalSettings(updates: Partial<EmailGlobalSettings>) {
|
|
116
|
+
Object.assign(globalSettings.value, updates)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Actions - Bulk Operations
|
|
120
|
+
function setBlocks(newBlocks: EmailBlock[]) {
|
|
121
|
+
blocks.value = newBlocks
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function clearBlocks() {
|
|
125
|
+
blocks.value = []
|
|
126
|
+
selectedBlockId.value = null
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function resetGlobalSettings() {
|
|
130
|
+
globalSettings.value = {}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function resetEditor() {
|
|
134
|
+
blocks.value = []
|
|
135
|
+
selectedBlockId.value = null
|
|
136
|
+
brand.value = null
|
|
137
|
+
resetGlobalSettings()
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function setBrand(newBrand: BrandFromDb | null) {
|
|
141
|
+
brand.value = newBrand
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function fetchBrands(options?: {
|
|
145
|
+
sort_by?: 'created_at' | 'name'
|
|
146
|
+
direction?: 'asc' | 'desc'
|
|
147
|
+
}) {
|
|
148
|
+
brands.value = await brandsApi.getAll(options)
|
|
149
|
+
return brands.value
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Set brand for new template (oldest) or existing template (by id, or null) */
|
|
153
|
+
async function initBrand(brandIdToRestore?: string | null) {
|
|
154
|
+
const list = await fetchBrands({ sort_by: 'created_at', direction: 'asc' })
|
|
155
|
+
if (brandIdToRestore) {
|
|
156
|
+
brand.value = list.find((b) => b.id === brandIdToRestore) ?? null
|
|
157
|
+
} else if (brandIdToRestore === undefined && list.length) {
|
|
158
|
+
// Create mode: default to oldest brand
|
|
159
|
+
brand.value = list[0]
|
|
160
|
+
}
|
|
161
|
+
// brandIdToRestore === null → keep null (explicit no-brand)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Data Loading/Export
|
|
165
|
+
function loadFromData(data: PostEntity<EmailMessage>) {
|
|
166
|
+
blocks.value = data.body.blocks || []
|
|
167
|
+
globalSettings.value = data.body.settings || {}
|
|
168
|
+
selectedBlockId.value = null
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function toJSON(): { blocks: EmailBlock[]; settings: EmailGlobalSettings } {
|
|
172
|
+
return {
|
|
173
|
+
blocks: blocks.value,
|
|
174
|
+
settings: globalSettings.value,
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
// State
|
|
180
|
+
blocks,
|
|
181
|
+
selectedBlockId,
|
|
182
|
+
globalSettings,
|
|
183
|
+
brands,
|
|
184
|
+
brand,
|
|
185
|
+
brandId,
|
|
186
|
+
brandColors,
|
|
187
|
+
resolvedSettings,
|
|
188
|
+
|
|
189
|
+
// Computed
|
|
190
|
+
selectedBlock,
|
|
191
|
+
hasBlocks,
|
|
192
|
+
|
|
193
|
+
// Actions - CRUD
|
|
194
|
+
addBlock,
|
|
195
|
+
insertBlock,
|
|
196
|
+
removeBlock,
|
|
197
|
+
updateBlock,
|
|
198
|
+
duplicateBlock,
|
|
199
|
+
|
|
200
|
+
// Actions - Selection
|
|
201
|
+
selectBlock,
|
|
202
|
+
clearSelection,
|
|
203
|
+
|
|
204
|
+
// Actions - Settings
|
|
205
|
+
updateGlobalSettings,
|
|
206
|
+
resetGlobalSettings,
|
|
207
|
+
setBrand,
|
|
208
|
+
fetchBrands,
|
|
209
|
+
initBrand,
|
|
210
|
+
|
|
211
|
+
// Actions - Bulk
|
|
212
|
+
setBlocks,
|
|
213
|
+
clearBlocks,
|
|
214
|
+
resetEditor,
|
|
215
|
+
|
|
216
|
+
// Data Import/Export
|
|
217
|
+
loadFromData,
|
|
218
|
+
toJSON,
|
|
219
|
+
}
|
|
220
|
+
})
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { SubmissionJob } from '@dev.smartpricing/message-composer-utils/types'
|
|
2
|
+
import { metaApi } from '~/api/meta'
|
|
3
|
+
import { useQueryCache } from '@pinia/colada'
|
|
4
|
+
import { useAppContextStore } from './appContext'
|
|
5
|
+
|
|
6
|
+
interface ActiveJob {
|
|
7
|
+
jobId: string
|
|
8
|
+
messageGroupId: string
|
|
9
|
+
startedAt: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const useMetaSubmissionStore = () => {
|
|
13
|
+
const toast = useToast()
|
|
14
|
+
const queryCache = useQueryCache()
|
|
15
|
+
const appContext = useAppContextStore()
|
|
16
|
+
const { t } = useI18n()
|
|
17
|
+
|
|
18
|
+
const activeJobs = useState<Map<string, ActiveJob>>('meta-submission-jobs', () => new Map())
|
|
19
|
+
const pollingInterval = useState<NodeJS.Timeout | null>('meta-polling-interval', () => null)
|
|
20
|
+
|
|
21
|
+
const isConfigured = computed(() => !!appContext.metaWabaId)
|
|
22
|
+
|
|
23
|
+
const submitToMeta = async (messageGroupId: string) => {
|
|
24
|
+
if (!isConfigured.value) {
|
|
25
|
+
throw new Error('Meta configuration not available')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const response = await metaApi.submitGroup(messageGroupId)
|
|
30
|
+
startBackgroundPolling(response.jobId, messageGroupId)
|
|
31
|
+
return response
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.error('Failed to submit to Meta:', error)
|
|
34
|
+
throw error
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const startBackgroundPolling = (jobId: string, messageGroupId: string) => {
|
|
39
|
+
// Add job to active jobs
|
|
40
|
+
activeJobs.value.set(jobId, {
|
|
41
|
+
jobId,
|
|
42
|
+
messageGroupId,
|
|
43
|
+
startedAt: Date.now(),
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
// Start polling if not already running
|
|
47
|
+
if (!pollingInterval.value) {
|
|
48
|
+
pollingInterval.value = setInterval(pollAllJobs, 10000) // 10 seconds
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const pollAllJobs = async () => {
|
|
53
|
+
if (activeJobs.value.size === 0) {
|
|
54
|
+
stopPolling()
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const jobsToCheck = Array.from(activeJobs.value.values())
|
|
59
|
+
|
|
60
|
+
for (const job of jobsToCheck) {
|
|
61
|
+
try {
|
|
62
|
+
const status = await metaApi.getJobStatus(job.jobId)
|
|
63
|
+
|
|
64
|
+
if (status.status === 'completed' || status.status === 'failed') {
|
|
65
|
+
handleJobComplete(status, job.messageGroupId)
|
|
66
|
+
activeJobs.value.delete(job.jobId)
|
|
67
|
+
}
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.error(`Failed to poll job ${job.jobId}:`, error)
|
|
70
|
+
// Remove job after 5 minutes of failed polling
|
|
71
|
+
if (Date.now() - job.startedAt > 5 * 60 * 1000) {
|
|
72
|
+
activeJobs.value.delete(job.jobId)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const handleJobComplete = (status: SubmissionJob, messageGroupId: string) => {
|
|
79
|
+
// Invalidate message group cache
|
|
80
|
+
queryCache.invalidateQueries({
|
|
81
|
+
key: ['message-groups', messageGroupId],
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
// Show notification
|
|
85
|
+
if (status.failed_count === 0) {
|
|
86
|
+
toast.add({
|
|
87
|
+
color: 'success',
|
|
88
|
+
title: t('meta_submission.success'),
|
|
89
|
+
})
|
|
90
|
+
} else {
|
|
91
|
+
toast.add({
|
|
92
|
+
color: 'error',
|
|
93
|
+
title: t('meta_submission.error'),
|
|
94
|
+
description: t('meta_submission.failed_count', { count: status.failed_count }),
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const stopPolling = () => {
|
|
100
|
+
if (pollingInterval.value) {
|
|
101
|
+
clearInterval(pollingInterval.value)
|
|
102
|
+
pollingInterval.value = null
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const hasActiveJobs = computed(() => activeJobs.value.size > 0)
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
activeJobs,
|
|
110
|
+
hasActiveJobs,
|
|
111
|
+
isConfigured,
|
|
112
|
+
submitToMeta,
|
|
113
|
+
startBackgroundPolling,
|
|
114
|
+
stopPolling,
|
|
115
|
+
}
|
|
116
|
+
}
|