@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,212 @@
|
|
|
1
|
+
import { ref, computed, watch, toRaw, type MaybeRef } from 'vue'
|
|
2
|
+
import { deepClone, deepEqual } from '@dev.smartpricing/message-composer-utils/utils'
|
|
3
|
+
import type { WhatsappMessage } from '@dev.smartpricing/message-composer-utils/types'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Storage key pattern for change tracking
|
|
7
|
+
*/
|
|
8
|
+
function getStorageKey(messageGroupId: string | number): string {
|
|
9
|
+
return `message-changes-${messageGroupId}`
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Tracked message state stored in localStorage
|
|
14
|
+
*/
|
|
15
|
+
interface TrackedMessageState {
|
|
16
|
+
header: WhatsappMessage['body']['header']
|
|
17
|
+
body: WhatsappMessage['body']['body']
|
|
18
|
+
footer: WhatsappMessage['body']['footer']
|
|
19
|
+
buttons: WhatsappMessage['body']['buttons']
|
|
20
|
+
timestamp: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Storage structure: keyed by language ID
|
|
25
|
+
*/
|
|
26
|
+
type ChangeTrackingStorage = Record<string, TrackedMessageState>
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Composable for tracking changes to messages with localStorage persistence
|
|
30
|
+
*
|
|
31
|
+
* @param messageGroupId - Reactive reference to message group ID
|
|
32
|
+
* @returns API methods for change tracking
|
|
33
|
+
*/
|
|
34
|
+
export function useChangeTracking(messageGroupId: MaybeRef<string | number>) {
|
|
35
|
+
const groupId = computed(() =>
|
|
36
|
+
typeof messageGroupId === 'object' && 'value' in messageGroupId
|
|
37
|
+
? messageGroupId.value
|
|
38
|
+
: messageGroupId,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
// Internal state: stores original message states per language
|
|
42
|
+
const originalStates = ref<ChangeTrackingStorage>({})
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Load original states from localStorage
|
|
46
|
+
*/
|
|
47
|
+
function loadFromStorage(): void {
|
|
48
|
+
try {
|
|
49
|
+
const key = getStorageKey(groupId.value)
|
|
50
|
+
const stored = localStorage.getItem(key)
|
|
51
|
+
if (stored) {
|
|
52
|
+
originalStates.value = JSON.parse(stored)
|
|
53
|
+
} else {
|
|
54
|
+
originalStates.value = {}
|
|
55
|
+
}
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.error('Failed to load change tracking from storage:', error)
|
|
58
|
+
originalStates.value = {}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Save original states to localStorage
|
|
64
|
+
*/
|
|
65
|
+
function saveToStorage(): void {
|
|
66
|
+
try {
|
|
67
|
+
const key = getStorageKey(groupId.value)
|
|
68
|
+
localStorage.setItem(key, JSON.stringify(originalStates.value))
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.error('Failed to save change tracking to storage:', error)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Record original state for a message
|
|
76
|
+
* Stores header, body, footer, buttons, and timestamp
|
|
77
|
+
*
|
|
78
|
+
* @param language - Language ID
|
|
79
|
+
* @param message - Message to track
|
|
80
|
+
*/
|
|
81
|
+
function recordOriginalState(language: string, message: WhatsappMessage): void {
|
|
82
|
+
// Use toRaw to remove Vue reactivity before cloning
|
|
83
|
+
const rawMessage = toRaw(message)
|
|
84
|
+
originalStates.value[language] = {
|
|
85
|
+
header: deepClone(toRaw(rawMessage.body.header)),
|
|
86
|
+
body: deepClone(toRaw(rawMessage.body.body)),
|
|
87
|
+
footer: deepClone(toRaw(rawMessage.body.footer)),
|
|
88
|
+
buttons: deepClone(toRaw(rawMessage.body.buttons)),
|
|
89
|
+
timestamp: Date.now(),
|
|
90
|
+
}
|
|
91
|
+
saveToStorage()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Check if a message has any changes compared to original state
|
|
96
|
+
*
|
|
97
|
+
* @param language - Language ID
|
|
98
|
+
* @param currentMessage - Current message to compare
|
|
99
|
+
* @returns true if message has changes, false otherwise
|
|
100
|
+
*/
|
|
101
|
+
function hasChanges(language: string, currentMessage: WhatsappMessage): boolean {
|
|
102
|
+
const original = originalStates.value[language]
|
|
103
|
+
if (!original) return false
|
|
104
|
+
|
|
105
|
+
// Use toRaw to remove Vue reactivity before comparison
|
|
106
|
+
const rawMessage = toRaw(currentMessage)
|
|
107
|
+
|
|
108
|
+
// Compare each field using deep equality
|
|
109
|
+
const headerChanged = !deepEqual(original.header, toRaw(rawMessage.body.header))
|
|
110
|
+
const bodyChanged = !deepEqual(original.body, toRaw(rawMessage.body.body))
|
|
111
|
+
const footerChanged = !deepEqual(original.footer, toRaw(rawMessage.body.footer))
|
|
112
|
+
const buttonsChanged = !deepEqual(original.buttons, toRaw(rawMessage.body.buttons))
|
|
113
|
+
|
|
114
|
+
return headerChanged || bodyChanged || footerChanged || buttonsChanged
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Get array of changed field names
|
|
119
|
+
*
|
|
120
|
+
* @param language - Language ID
|
|
121
|
+
* @param currentMessage - Current message to compare
|
|
122
|
+
* @returns Array of field names that have changed
|
|
123
|
+
*/
|
|
124
|
+
function getChangedFields(language: string, currentMessage: WhatsappMessage): string[] {
|
|
125
|
+
const original = originalStates.value[language]
|
|
126
|
+
if (!original) return []
|
|
127
|
+
|
|
128
|
+
// Use toRaw to remove Vue reactivity before comparison
|
|
129
|
+
const rawMessage = toRaw(currentMessage)
|
|
130
|
+
const changedFields: string[] = []
|
|
131
|
+
|
|
132
|
+
if (!deepEqual(original.header, toRaw(rawMessage.body.header))) {
|
|
133
|
+
changedFields.push('header')
|
|
134
|
+
}
|
|
135
|
+
if (!deepEqual(original.body, toRaw(rawMessage.body.body))) {
|
|
136
|
+
changedFields.push('body')
|
|
137
|
+
}
|
|
138
|
+
if (!deepEqual(original.footer, toRaw(rawMessage.body.footer))) {
|
|
139
|
+
changedFields.push('footer')
|
|
140
|
+
}
|
|
141
|
+
if (!deepEqual(original.buttons, toRaw(rawMessage.body.buttons))) {
|
|
142
|
+
changedFields.push('buttons')
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return changedFields
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Get array of language IDs that have changes
|
|
150
|
+
*
|
|
151
|
+
* @param messages - Record of all messages by language ID
|
|
152
|
+
* @returns Array of language IDs with changes
|
|
153
|
+
*/
|
|
154
|
+
function getLanguagesWithChanges(messages: Record<string, WhatsappMessage>): string[] {
|
|
155
|
+
return Object.keys(messages).filter(
|
|
156
|
+
(languageId) => messages[languageId] && hasChanges(languageId, messages[languageId]),
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Get count of changed fields for a specific language
|
|
162
|
+
*
|
|
163
|
+
* @param language - Language ID
|
|
164
|
+
* @param currentMessage - Current message to compare
|
|
165
|
+
* @returns Number of changed fields
|
|
166
|
+
*/
|
|
167
|
+
function getChangeCount(language: string, currentMessage: WhatsappMessage): number {
|
|
168
|
+
return getChangedFields(language, currentMessage).length
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Clear changes for a specific language
|
|
173
|
+
*
|
|
174
|
+
* @param language - Language ID to clear
|
|
175
|
+
*/
|
|
176
|
+
function clearChanges(language: string): void {
|
|
177
|
+
delete originalStates.value[language]
|
|
178
|
+
saveToStorage()
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Clear all changes for this message group
|
|
183
|
+
*/
|
|
184
|
+
function clearAllChanges(): void {
|
|
185
|
+
originalStates.value = {}
|
|
186
|
+
try {
|
|
187
|
+
const key = getStorageKey(groupId.value)
|
|
188
|
+
localStorage.removeItem(key)
|
|
189
|
+
} catch (error) {
|
|
190
|
+
console.error('Failed to clear change tracking from storage:', error)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Watch for messageGroupId changes and reload from storage
|
|
195
|
+
watch(
|
|
196
|
+
groupId,
|
|
197
|
+
() => {
|
|
198
|
+
loadFromStorage()
|
|
199
|
+
},
|
|
200
|
+
{ immediate: true },
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
recordOriginalState,
|
|
205
|
+
hasChanges,
|
|
206
|
+
getChangedFields,
|
|
207
|
+
getLanguagesWithChanges,
|
|
208
|
+
getChangeCount,
|
|
209
|
+
clearChanges,
|
|
210
|
+
clearAllChanges,
|
|
211
|
+
}
|
|
212
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { ref, computed, watch, toRaw, type MaybeRef } from 'vue'
|
|
2
|
+
import { deepClone, deepEqual } from '@dev.smartpricing/message-composer-utils/utils'
|
|
3
|
+
import type { EmailMessage, EmailBlock } from '@dev.smartpricing/message-composer-utils/types'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Storage key pattern for email change tracking
|
|
7
|
+
*/
|
|
8
|
+
function getStorageKey(messageGroupId: string | number): string {
|
|
9
|
+
return `email-message-changes-${messageGroupId}`
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Tracked email message state stored in localStorage
|
|
14
|
+
*/
|
|
15
|
+
interface TrackedEmailState {
|
|
16
|
+
subject: string
|
|
17
|
+
blocks: EmailBlock[]
|
|
18
|
+
timestamp: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Storage structure: keyed by language ID
|
|
23
|
+
*/
|
|
24
|
+
type EmailChangeTrackingStorage = Record<string, TrackedEmailState>
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Composable for tracking changes to email messages with localStorage persistence
|
|
28
|
+
*
|
|
29
|
+
* @param messageGroupId - Reactive reference to message group ID
|
|
30
|
+
* @returns API methods for change tracking
|
|
31
|
+
*/
|
|
32
|
+
export function useChangeTrackingEmail(messageGroupId: MaybeRef<string | number>) {
|
|
33
|
+
const groupId = computed(() =>
|
|
34
|
+
typeof messageGroupId === 'object' && 'value' in messageGroupId
|
|
35
|
+
? messageGroupId.value
|
|
36
|
+
: messageGroupId,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
// Internal state: stores original message states per language
|
|
40
|
+
const originalStates = ref<EmailChangeTrackingStorage>({})
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Load original states from localStorage
|
|
44
|
+
*/
|
|
45
|
+
function loadFromStorage(): void {
|
|
46
|
+
try {
|
|
47
|
+
const key = getStorageKey(groupId.value)
|
|
48
|
+
const stored = localStorage.getItem(key)
|
|
49
|
+
if (stored) {
|
|
50
|
+
originalStates.value = JSON.parse(stored)
|
|
51
|
+
} else {
|
|
52
|
+
originalStates.value = {}
|
|
53
|
+
}
|
|
54
|
+
} catch (error) {
|
|
55
|
+
console.error('Failed to load email change tracking from storage:', error)
|
|
56
|
+
originalStates.value = {}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Save original states to localStorage
|
|
62
|
+
*/
|
|
63
|
+
function saveToStorage(): void {
|
|
64
|
+
try {
|
|
65
|
+
const key = getStorageKey(groupId.value)
|
|
66
|
+
localStorage.setItem(key, JSON.stringify(originalStates.value))
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.error('Failed to save email change tracking to storage:', error)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Record original state for an email message
|
|
74
|
+
* Stores subject, blocks, and timestamp
|
|
75
|
+
*
|
|
76
|
+
* @param language - Language ID
|
|
77
|
+
* @param message - Email message to track
|
|
78
|
+
*/
|
|
79
|
+
function recordOriginalState(language: string, message: EmailMessage): void {
|
|
80
|
+
// Use toRaw to remove Vue reactivity before cloning
|
|
81
|
+
const rawMessage = toRaw(message)
|
|
82
|
+
originalStates.value[language] = {
|
|
83
|
+
subject: rawMessage.body.subject,
|
|
84
|
+
blocks: deepClone(toRaw(rawMessage.body.blocks)),
|
|
85
|
+
timestamp: Date.now(),
|
|
86
|
+
}
|
|
87
|
+
saveToStorage()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Check if a message has any changes compared to original state
|
|
92
|
+
*
|
|
93
|
+
* @param language - Language ID
|
|
94
|
+
* @param currentMessage - Current email message to compare
|
|
95
|
+
* @returns true if message has changes, false otherwise
|
|
96
|
+
*/
|
|
97
|
+
function hasChanges(language: string, currentMessage: EmailMessage): boolean {
|
|
98
|
+
const original = originalStates.value[language]
|
|
99
|
+
if (!original) return false
|
|
100
|
+
|
|
101
|
+
// Use toRaw to remove Vue reactivity before comparison
|
|
102
|
+
const rawMessage = toRaw(currentMessage)
|
|
103
|
+
|
|
104
|
+
// Compare subject
|
|
105
|
+
const subjectChanged = original.subject !== rawMessage.body.subject
|
|
106
|
+
|
|
107
|
+
// Compare blocks using deep equality
|
|
108
|
+
const blocksChanged = !deepEqual(original.blocks, toRaw(rawMessage.body.blocks))
|
|
109
|
+
|
|
110
|
+
return subjectChanged || blocksChanged
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Get array of changed block IDs
|
|
115
|
+
*
|
|
116
|
+
* @param language - Language ID
|
|
117
|
+
* @param currentMessage - Current email message to compare
|
|
118
|
+
* @returns Array of block IDs that have changed
|
|
119
|
+
*/
|
|
120
|
+
function getChangedBlockIds(language: string, currentMessage: EmailMessage): string[] {
|
|
121
|
+
const original = originalStates.value[language]
|
|
122
|
+
if (!original) return []
|
|
123
|
+
|
|
124
|
+
// Use toRaw to remove Vue reactivity before comparison
|
|
125
|
+
const rawMessage = toRaw(currentMessage)
|
|
126
|
+
const changedBlockIds: string[] = []
|
|
127
|
+
|
|
128
|
+
// Compare blocks by ID
|
|
129
|
+
const originalBlocksMap = new Map(original.blocks.map((b) => [b.id, b]))
|
|
130
|
+
const currentBlocksMap = new Map(rawMessage.body.blocks.map((b) => [b.id, toRaw(b)]))
|
|
131
|
+
|
|
132
|
+
// Check for changes
|
|
133
|
+
for (const [id, currentBlock] of currentBlocksMap) {
|
|
134
|
+
const originalBlock = originalBlocksMap.get(id)
|
|
135
|
+
if (!originalBlock || !deepEqual(originalBlock, currentBlock)) {
|
|
136
|
+
changedBlockIds.push(id)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Note: Nested grid blocks are part of their parent grid block change detection
|
|
141
|
+
|
|
142
|
+
return changedBlockIds
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Check if subject has changed
|
|
147
|
+
*
|
|
148
|
+
* @param language - Language ID
|
|
149
|
+
* @param currentMessage - Current email message to compare
|
|
150
|
+
* @returns true if subject changed
|
|
151
|
+
*/
|
|
152
|
+
function hasSubjectChanged(language: string, currentMessage: EmailMessage): boolean {
|
|
153
|
+
const original = originalStates.value[language]
|
|
154
|
+
if (!original) return false
|
|
155
|
+
return original.subject !== currentMessage.body.subject
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Get array of language IDs that have changes
|
|
160
|
+
*
|
|
161
|
+
* @param messages - Record of all email messages by language ID
|
|
162
|
+
* @returns Array of language IDs with changes
|
|
163
|
+
*/
|
|
164
|
+
function getLanguagesWithChanges(messages: Record<string, EmailMessage>): string[] {
|
|
165
|
+
return Object.keys(messages).filter(
|
|
166
|
+
(languageId) => messages[languageId] && hasChanges(languageId, messages[languageId]),
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Get count of changed blocks for a specific language
|
|
172
|
+
*
|
|
173
|
+
* @param language - Language ID
|
|
174
|
+
* @param currentMessage - Current email message to compare
|
|
175
|
+
* @returns Number of changed blocks (includes subject if changed)
|
|
176
|
+
*/
|
|
177
|
+
function getChangeCount(language: string, currentMessage: EmailMessage): number {
|
|
178
|
+
let count = getChangedBlockIds(language, currentMessage).length
|
|
179
|
+
if (hasSubjectChanged(language, currentMessage)) {
|
|
180
|
+
count++
|
|
181
|
+
}
|
|
182
|
+
return count
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Clear changes for a specific language
|
|
187
|
+
*
|
|
188
|
+
* @param language - Language ID to clear
|
|
189
|
+
*/
|
|
190
|
+
function clearChanges(language: string): void {
|
|
191
|
+
delete originalStates.value[language]
|
|
192
|
+
saveToStorage()
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Clear all changes for this message group
|
|
197
|
+
*/
|
|
198
|
+
function clearAllChanges(): void {
|
|
199
|
+
originalStates.value = {}
|
|
200
|
+
try {
|
|
201
|
+
const key = getStorageKey(groupId.value)
|
|
202
|
+
localStorage.removeItem(key)
|
|
203
|
+
} catch (error) {
|
|
204
|
+
console.error('Failed to clear email change tracking from storage:', error)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Watch for messageGroupId changes and reload from storage
|
|
209
|
+
watch(
|
|
210
|
+
groupId,
|
|
211
|
+
() => {
|
|
212
|
+
loadFromStorage()
|
|
213
|
+
},
|
|
214
|
+
{ immediate: true },
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
recordOriginalState,
|
|
219
|
+
hasChanges,
|
|
220
|
+
getChangedBlockIds,
|
|
221
|
+
hasSubjectChanged,
|
|
222
|
+
getLanguagesWithChanges,
|
|
223
|
+
getChangeCount,
|
|
224
|
+
clearChanges,
|
|
225
|
+
clearAllChanges,
|
|
226
|
+
}
|
|
227
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { computed } from 'vue'
|
|
2
|
+
import type { RenderType } from '@dev.smartpricing/message-composer-utils/types'
|
|
3
|
+
import { useAppContextStore } from '@/stores/appContext'
|
|
4
|
+
|
|
5
|
+
export function useChannels() {
|
|
6
|
+
const appContext = useAppContextStore()
|
|
7
|
+
|
|
8
|
+
const hasWhatsapp = computed(() => appContext.channels.includes('whatsapp'))
|
|
9
|
+
const hasEmail = computed(() => appContext.channels.includes('email'))
|
|
10
|
+
|
|
11
|
+
const allowedRenderTypes = computed<RenderType[]>(() => {
|
|
12
|
+
const types: RenderType[] = []
|
|
13
|
+
if (hasWhatsapp.value) types.push('whatsapp_v1')
|
|
14
|
+
if (hasEmail.value) types.push('email_v1')
|
|
15
|
+
return types
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
const defaultRenderType = computed<RenderType>(() => allowedRenderTypes.value[0] || 'whatsapp_v1')
|
|
19
|
+
|
|
20
|
+
function isChannelAllowed(channel: 'whatsapp' | 'email'): boolean {
|
|
21
|
+
return appContext.channels.includes(channel)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isRenderTypeAllowed(renderType: RenderType): boolean {
|
|
25
|
+
return allowedRenderTypes.value.includes(renderType)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
hasWhatsapp,
|
|
30
|
+
hasEmail,
|
|
31
|
+
allowedRenderTypes,
|
|
32
|
+
defaultRenderType,
|
|
33
|
+
isChannelAllowed,
|
|
34
|
+
isRenderTypeAllowed,
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { EmailBlock } from '@dev.smartpricing/message-composer-utils/types'
|
|
2
|
+
import type { JSONContent } from '@tiptap/vue-3'
|
|
3
|
+
|
|
4
|
+
function extractTextFromJSON(node: JSONContent | undefined): string {
|
|
5
|
+
if (!node) return ''
|
|
6
|
+
let text = ''
|
|
7
|
+
if (node.text) text += node.text
|
|
8
|
+
if (node.content) {
|
|
9
|
+
for (const child of node.content) {
|
|
10
|
+
text += extractTextFromJSON(child)
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return text
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getBlockText(block: EmailBlock): string {
|
|
17
|
+
if (block.type === 'heading' || block.type === 'paragraph') {
|
|
18
|
+
const content = (block.settings as { content?: JSONContent }).content
|
|
19
|
+
return extractTextFromJSON(content)
|
|
20
|
+
}
|
|
21
|
+
if (block.type === 'button') {
|
|
22
|
+
return (block.settings as { text?: string }).text || ''
|
|
23
|
+
}
|
|
24
|
+
return ''
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function useEmailBlockContext(currentBlockId: string) {
|
|
28
|
+
const emailEditorStore = useEmailEditorStore()
|
|
29
|
+
|
|
30
|
+
const context = computed(() => {
|
|
31
|
+
const parts: string[] = []
|
|
32
|
+
for (const block of emailEditorStore.blocks) {
|
|
33
|
+
if (block.id === currentBlockId) continue
|
|
34
|
+
const text = getBlockText(block).trim()
|
|
35
|
+
if (text) {
|
|
36
|
+
parts.push(`[${block.type}]: ${text}`)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return parts.join('\n') || undefined
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
return { context }
|
|
43
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { ref, unref, type ComputedRef } from 'vue'
|
|
2
|
+
import { type RouteLocationRaw } from 'vue-router'
|
|
3
|
+
|
|
4
|
+
export function useExitConfirmation(shouldBlockNavigation: ComputedRef<boolean>) {
|
|
5
|
+
const isForcingNavigation = ref(false)
|
|
6
|
+
const isNavigatingTo = ref<RouteLocationRaw | null>(null)
|
|
7
|
+
|
|
8
|
+
const { confirm } = useConfirm()
|
|
9
|
+
const { t } = useI18n()
|
|
10
|
+
|
|
11
|
+
// Setup route guard
|
|
12
|
+
onBeforeRouteLeave((_to, _from, next) => {
|
|
13
|
+
const _isForcedNavigation = unref(isForcingNavigation)
|
|
14
|
+
isForcingNavigation.value = false
|
|
15
|
+
|
|
16
|
+
if (shouldBlockNavigation.value && !_isForcedNavigation) {
|
|
17
|
+
isNavigatingTo.value = _to
|
|
18
|
+
next(false)
|
|
19
|
+
|
|
20
|
+
confirm({
|
|
21
|
+
title: t('dialogs.unsaved_changes.title'),
|
|
22
|
+
message: t('dialogs.unsaved_changes.message'),
|
|
23
|
+
confirmLabel: t('dialogs.unsaved_changes.leave'),
|
|
24
|
+
cancelLabel: t('dialogs.unsaved_changes.cancel'),
|
|
25
|
+
action: () => {
|
|
26
|
+
const target = isNavigatingTo.value
|
|
27
|
+
if (target) {
|
|
28
|
+
isForcingNavigation.value = true
|
|
29
|
+
navigateTo(target)
|
|
30
|
+
isNavigatingTo.value = null
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
})
|
|
34
|
+
} else {
|
|
35
|
+
isNavigatingTo.value = null
|
|
36
|
+
next()
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { RouteLocationRaw } from 'vue-router'
|
|
2
|
+
|
|
3
|
+
export function useGoBack() {
|
|
4
|
+
const router = useRouter()
|
|
5
|
+
|
|
6
|
+
function goBackOrFallback(fallback: RouteLocationRaw = '/') {
|
|
7
|
+
if (router.options.history.state.back) {
|
|
8
|
+
router.back()
|
|
9
|
+
} else {
|
|
10
|
+
navigateTo(fallback, { replace: true })
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return { goBackOrFallback }
|
|
15
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { computed, type Ref } from 'vue'
|
|
2
|
+
import type {
|
|
3
|
+
WhatsappMessage,
|
|
4
|
+
LanguageFromDb,
|
|
5
|
+
} from '@dev.smartpricing/message-composer-utils/types'
|
|
6
|
+
import { calculateWhatsappTranslationPercentage } from '@dev.smartpricing/message-composer-utils/utils'
|
|
7
|
+
|
|
8
|
+
interface UseIncompleteTranslationsParams {
|
|
9
|
+
messages: Ref<Record<string, WhatsappMessage>>
|
|
10
|
+
languages: Ref<LanguageFromDb[] | undefined>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function useIncompleteTranslations(params: UseIncompleteTranslationsParams) {
|
|
14
|
+
const { messages, languages } = params
|
|
15
|
+
|
|
16
|
+
const { mainLanguage } = storeToRefs(useAppContextStore())
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Get list of languages with incomplete translations
|
|
20
|
+
*/
|
|
21
|
+
const incompleteLanguages = computed(() => {
|
|
22
|
+
if (!languages.value) return []
|
|
23
|
+
|
|
24
|
+
const baseLang = resolveDefaultLanguage(messages.value, mainLanguage.value)
|
|
25
|
+
const baseMessage = baseLang ? messages.value[baseLang] : undefined
|
|
26
|
+
if (!baseMessage) return []
|
|
27
|
+
|
|
28
|
+
const incomplete: Array<{ language: LanguageFromDb; percentage: number }> = []
|
|
29
|
+
|
|
30
|
+
for (const lang of languages.value) {
|
|
31
|
+
const message = messages.value[lang.id]
|
|
32
|
+
if (!message) continue
|
|
33
|
+
|
|
34
|
+
const percentage = calculateWhatsappTranslationPercentage(message, baseMessage)
|
|
35
|
+
|
|
36
|
+
if (percentage < 100) {
|
|
37
|
+
incomplete.push({ language: lang, percentage })
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return incomplete
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Check if there are any incomplete translations
|
|
46
|
+
*/
|
|
47
|
+
const hasIncompleteTranslations = computed(() => {
|
|
48
|
+
return incompleteLanguages.value.length > 0
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
incompleteLanguages,
|
|
53
|
+
hasIncompleteTranslations,
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { computed, type Ref } from 'vue'
|
|
2
|
+
import type {
|
|
3
|
+
EmailMessage,
|
|
4
|
+
LanguageFromDb,
|
|
5
|
+
PostEntity,
|
|
6
|
+
} from '@dev.smartpricing/message-composer-utils/types'
|
|
7
|
+
import { calculateEmailTranslationPercentage } from '@dev.smartpricing/message-composer-utils/utils'
|
|
8
|
+
|
|
9
|
+
interface UseIncompleteTranslationsEmailParams {
|
|
10
|
+
messages: Ref<Record<string, EmailMessage | PostEntity<EmailMessage>>>
|
|
11
|
+
languages: Ref<LanguageFromDb[] | undefined>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function useIncompleteTranslationsEmail(params: UseIncompleteTranslationsEmailParams) {
|
|
15
|
+
const { messages, languages } = params
|
|
16
|
+
|
|
17
|
+
const { mainLanguage } = storeToRefs(useAppContextStore())
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get list of languages with incomplete translations
|
|
21
|
+
*/
|
|
22
|
+
const incompleteLanguages = computed(() => {
|
|
23
|
+
if (!languages.value) return []
|
|
24
|
+
|
|
25
|
+
const baseLang = resolveDefaultLanguage(messages.value, mainLanguage.value)
|
|
26
|
+
const baseMessage = baseLang ? messages.value[baseLang] : undefined
|
|
27
|
+
if (!baseMessage) return []
|
|
28
|
+
|
|
29
|
+
const incomplete: Array<{ language: LanguageFromDb; percentage: number }> = []
|
|
30
|
+
|
|
31
|
+
for (const lang of languages.value) {
|
|
32
|
+
const message = messages.value[lang.id]
|
|
33
|
+
if (!message) continue
|
|
34
|
+
|
|
35
|
+
const percentage = calculateEmailTranslationPercentage(message, baseMessage)
|
|
36
|
+
|
|
37
|
+
if (percentage < 100) {
|
|
38
|
+
incomplete.push({ language: lang, percentage })
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return incomplete
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Check if there are any incomplete translations
|
|
47
|
+
*/
|
|
48
|
+
const hasIncompleteTranslations = computed(() => {
|
|
49
|
+
return incompleteLanguages.value.length > 0
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
incompleteLanguages,
|
|
54
|
+
hasIncompleteTranslations,
|
|
55
|
+
}
|
|
56
|
+
}
|