@dev.smartpricing/message-composer-layer 1.0.16 → 1.0.18
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/app/components/Brand/BrandPreview.vue +6 -3
- package/app/components/Brand/Steps/BrandStepVisual.vue +1 -0
- package/app/components/Common/LanguageList.vue +2 -2
- package/app/components/Common/PhoneInput.vue +1 -1
- package/app/components/Common/PreviewById.vue +2 -2
- package/app/components/Composer/MessageLibrary.vue +6 -6
- package/app/components/DynamicDataSlideover.vue +2 -2
- package/app/components/Editor/RichTextEditor.vue +2 -2
- package/app/components/Email/Editor/Blocks/ButtonBlock.vue +2 -2
- package/app/components/Email/Editor/Blocks/GridSettings.vue +2 -2
- package/app/components/Email/Editor/Blocks/HeaderSettings.vue +1 -1
- package/app/components/Email/Editor/Blocks/SpacerSettings.vue +1 -1
- package/app/components/Email/Editor/GeneralStyleSettings.vue +2 -2
- package/app/components/Email/Editor/Index.vue +1 -1
- package/app/components/Email/InlinePreview.vue +1 -1
- package/app/components/Email/PreviewPanel.vue +1 -1
- package/app/components/Email/SendTestEmailModal.vue +3 -1
- package/app/components/Email/Translator/Index.vue +3 -3
- package/app/components/Whatsapp/CategoryChanger.vue +4 -2
- package/app/components/Whatsapp/Editor/Buttons.vue +6 -1
- package/app/components/Whatsapp/Editor/Index.vue +2 -1
- package/app/components/Whatsapp/Translator/Button.vue +2 -2
- package/app/composables/composerContext.ts +3 -1
- package/app/composables/useAutoTranslateEmail.ts +10 -6
- package/app/composables/useChangeTrackingEmail.ts +13 -3
- package/app/composables/useExitConfirmation.ts +2 -2
- package/app/composables/useMediaLimits.ts +1 -1
- package/app/composables/useTemplateFormValidation.ts +6 -1
- package/app/composables/useTemplateOperations.ts +4 -1
- package/app/composables/useTranslationFiltersEmail.ts +9 -3
- package/app/pages/preview/[id].vue +2 -2
- package/app/queries/brands.ts +1 -1
- package/app/stores/emailEditor.ts +2 -2
- package/package.json +3 -2
|
@@ -14,6 +14,9 @@ import {
|
|
|
14
14
|
DEFAULT_BUTTON_SETTINGS,
|
|
15
15
|
DEFAULT_HEADING_SETTINGS,
|
|
16
16
|
emptyEmailBlock,
|
|
17
|
+
emptyHeadingBlock,
|
|
18
|
+
emptyParagraphBlock,
|
|
19
|
+
emptyButtonBlock,
|
|
17
20
|
} from '@dev.smartpricing/message-composer-utils/types'
|
|
18
21
|
import { useDebounceFn } from '@vueuse/core'
|
|
19
22
|
import HtmlPreviewIframe from '~/components/Common/HtmlPreviewIframe.vue'
|
|
@@ -41,14 +44,14 @@ const sampleMessage: PostEntity<EmailMessage> = {
|
|
|
41
44
|
blocks: [
|
|
42
45
|
emptyEmailBlock('header'),
|
|
43
46
|
{
|
|
44
|
-
...
|
|
47
|
+
...emptyHeadingBlock(),
|
|
45
48
|
settings: {
|
|
46
49
|
...DEFAULT_HEADING_SETTINGS,
|
|
47
50
|
content: createTipTapContent('<p>Lorem ipsum
dolor</p>', { preset: 'email' }),
|
|
48
51
|
},
|
|
49
52
|
},
|
|
50
53
|
{
|
|
51
|
-
...
|
|
54
|
+
...emptyParagraphBlock(),
|
|
52
55
|
settings: {
|
|
53
56
|
...DEFAULT_PARAGRAPH_SETTINGS,
|
|
54
57
|
content: createTipTapContent(
|
|
@@ -58,7 +61,7 @@ const sampleMessage: PostEntity<EmailMessage> = {
|
|
|
58
61
|
},
|
|
59
62
|
},
|
|
60
63
|
{
|
|
61
|
-
...
|
|
64
|
+
...emptyButtonBlock(),
|
|
62
65
|
settings: {
|
|
63
66
|
...DEFAULT_BUTTON_SETTINGS,
|
|
64
67
|
content: createTipTapContent('BUTTON', { preset: 'email' }),
|
|
@@ -38,6 +38,7 @@ async function extractColorsFromImage(file: File) {
|
|
|
38
38
|
img.onload = () => resolve()
|
|
39
39
|
img.onerror = reject
|
|
40
40
|
})
|
|
41
|
+
// @ts-expect-error colorthief types lack construct signature
|
|
41
42
|
const colorThief = new ColorThief()
|
|
42
43
|
const palette = colorThief.getPalette(img, 4)
|
|
43
44
|
const extracted = new Set(['#FFFFFF', '#000000'])
|
|
@@ -193,8 +193,8 @@ function askDeleteMessage(message: Message | PostEntity<Message>) {
|
|
|
193
193
|
title: t('dialogs.delete_message.title'),
|
|
194
194
|
message: t('dialogs.delete_message.message', { name: message.language_id }),
|
|
195
195
|
destructive: true,
|
|
196
|
-
|
|
197
|
-
|
|
196
|
+
confirmProps: { label: t('common.actions.delete') },
|
|
197
|
+
cancelProps: { label: t('common.actions.cancel') },
|
|
198
198
|
action: async () => {
|
|
199
199
|
if ('id' in message && 'message_group_id' in message) {
|
|
200
200
|
await deleteMessageMutation({
|
|
@@ -760,7 +760,7 @@ function updateNumber(value: number | undefined) {
|
|
|
760
760
|
}
|
|
761
761
|
|
|
762
762
|
const prefixes = Object.keys(PhoneCodes['en']).map((key) => ({
|
|
763
|
-
label: PhoneCodes['en'][key],
|
|
763
|
+
label: PhoneCodes['en'][key as unknown as keyof (typeof PhoneCodes)['en']],
|
|
764
764
|
value: key,
|
|
765
765
|
onSelect: () => {
|
|
766
766
|
updatePrefix(key)
|
|
@@ -42,7 +42,7 @@ const isEmail = computed(() => selectedMessage.value?.render_type === 'email_v1'
|
|
|
42
42
|
|
|
43
43
|
<EmailPreview
|
|
44
44
|
v-else-if="isEmail && selectedMessage"
|
|
45
|
-
:message="selectedMessage as EmailMessage"
|
|
45
|
+
:message="selectedMessage as unknown as EmailMessage"
|
|
46
46
|
:message-group-id="id"
|
|
47
47
|
:show-subject="showSubject"
|
|
48
48
|
:view="view"
|
|
@@ -52,7 +52,7 @@ const isEmail = computed(() => selectedMessage.value?.render_type === 'email_v1'
|
|
|
52
52
|
|
|
53
53
|
<WhatsappPreview
|
|
54
54
|
v-else-if="isWhatsapp && selectedMessage"
|
|
55
|
-
:message="selectedMessage as WhatsappMessage"
|
|
55
|
+
:message="selectedMessage as unknown as WhatsappMessage"
|
|
56
56
|
:dynamic-data="dynamicData"
|
|
57
57
|
/>
|
|
58
58
|
</div>
|
|
@@ -60,8 +60,8 @@ function askDeleteGroup(row: MessageGroupWithMessageSummary) {
|
|
|
60
60
|
title: t('dialogs.delete_message_group.title'),
|
|
61
61
|
message: t('dialogs.delete_message_group.message', { name: row.name }),
|
|
62
62
|
destructive: true,
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
confirmProps: { label: t('common.actions.delete') },
|
|
64
|
+
cancelProps: { label: t('common.actions.cancel') },
|
|
65
65
|
action: async () => {
|
|
66
66
|
await deleteGroup(row.id)
|
|
67
67
|
trackEvent('message_delete', getMessageTrackingProps(row))
|
|
@@ -81,8 +81,8 @@ function askPublishGroup(row: MessageGroupWithMessageSummary) {
|
|
|
81
81
|
confirm({
|
|
82
82
|
title: t('dialogs.publish_message_group.title'),
|
|
83
83
|
message: t('dialogs.publish_message_group.message', { name: row.name }),
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
confirmProps: { label: t('common.actions.publish') },
|
|
85
|
+
cancelProps: { label: t('common.actions.cancel') },
|
|
86
86
|
action: async () => {
|
|
87
87
|
await publishGroup(row.id)
|
|
88
88
|
notifyParent('TEMPLATE_PUBLISHED', { id: row.id, name: row.name })
|
|
@@ -101,8 +101,8 @@ function askUnpublishGroup(row: MessageGroupWithMessageSummary) {
|
|
|
101
101
|
confirm({
|
|
102
102
|
title: t('dialogs.unpublish_message_group.title'),
|
|
103
103
|
message: t('dialogs.unpublish_message_group.message', { name: row.name }),
|
|
104
|
-
|
|
105
|
-
|
|
104
|
+
confirmProps: { label: t('common.actions.unpublish') },
|
|
105
|
+
cancelProps: { label: t('common.actions.cancel') },
|
|
106
106
|
action: async () => {
|
|
107
107
|
await unpublishGroup(row.id)
|
|
108
108
|
notifyParent('TEMPLATE_UNPUBLISHED', { id: row.id, name: row.name })
|
|
@@ -28,7 +28,7 @@ watch(
|
|
|
28
28
|
if (props.defaultContext && contextKeys.value.includes(props.defaultContext)) {
|
|
29
29
|
activeTab.value = props.defaultContext
|
|
30
30
|
} else if (contextKeys.value.length > 0) {
|
|
31
|
-
activeTab.value = contextKeys.value[0]
|
|
31
|
+
activeTab.value = contextKeys.value[0]!
|
|
32
32
|
}
|
|
33
33
|
},
|
|
34
34
|
{ immediate: true },
|
|
@@ -58,7 +58,7 @@ const contextDescriptions = computed(() => {
|
|
|
58
58
|
.filter((ctx) => ctx.description)
|
|
59
59
|
})
|
|
60
60
|
|
|
61
|
-
const commandGroups = computed<
|
|
61
|
+
const commandGroups = computed<{ id: string; items: DynamicValueField[] }[]>(() => {
|
|
62
62
|
if (!data.value || !activeContextKey.value) return []
|
|
63
63
|
|
|
64
64
|
const fields = data.value[activeContextKey.value]?.fields || []
|
|
@@ -368,8 +368,8 @@ defineExpose({
|
|
|
368
368
|
content-type="json"
|
|
369
369
|
:image="false"
|
|
370
370
|
:extensions="extensions"
|
|
371
|
-
:starter-kit="starterKitConfig"
|
|
372
|
-
:on-update="handleUpdate"
|
|
371
|
+
:starter-kit="starterKitConfig as any"
|
|
372
|
+
:on-update="handleUpdate as any"
|
|
373
373
|
:mention="false"
|
|
374
374
|
:on-blur="handleBlur"
|
|
375
375
|
:placeholder="$t('editor.placeholder.insert_text')"
|
|
@@ -37,8 +37,8 @@ const buttonStyles = computed(() => {
|
|
|
37
37
|
const styles: Record<string, string> = {
|
|
38
38
|
fontFamily: `${fontFamily}, sans-serif`,
|
|
39
39
|
fontSize: `${props.block.settings.fontSize}px`,
|
|
40
|
-
color: props.block.settings.textColor ?? resolved.button.textColor,
|
|
41
|
-
backgroundColor: props.block.settings.backgroundColor ?? resolved.button.backgroundColor,
|
|
40
|
+
color: props.block.settings.textColor ?? resolved.button.textColor ?? '',
|
|
41
|
+
backgroundColor: props.block.settings.backgroundColor ?? resolved.button.backgroundColor ?? '',
|
|
42
42
|
textAlign: 'center',
|
|
43
43
|
borderRadius:
|
|
44
44
|
props.block.settings.shape === 'square'
|
|
@@ -36,8 +36,8 @@ function updateColumns(newColumns: 1 | 2 | 3) {
|
|
|
36
36
|
confirm({
|
|
37
37
|
title: t('dialogs.grid_reduce_columns.title'),
|
|
38
38
|
message: t('dialogs.grid_reduce_columns.message'),
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
confirmProps: { label: t('dialogs.grid_reduce_columns.confirm') },
|
|
40
|
+
cancelProps: { label: t('common.actions.cancel') },
|
|
41
41
|
action: () => applyColumnChange(newColumns),
|
|
42
42
|
})
|
|
43
43
|
return
|
|
@@ -14,7 +14,7 @@ const emailEditorStore = useEmailEditorStore()
|
|
|
14
14
|
const { t } = useI18n()
|
|
15
15
|
|
|
16
16
|
const src = computed({
|
|
17
|
-
get: () => props.block.settings.src || emailEditorStore.brand?.logo_url ||
|
|
17
|
+
get: () => props.block.settings.src || emailEditorStore.brand?.logo_url || '',
|
|
18
18
|
set: (value) => updateSettings({ src: value }),
|
|
19
19
|
})
|
|
20
20
|
|
|
@@ -20,8 +20,8 @@ defineOptions({
|
|
|
20
20
|
{{ $t('editor.email.generalStyle.brandSection') }}
|
|
21
21
|
</h3>
|
|
22
22
|
<USelectMenu
|
|
23
|
-
v-model="brand"
|
|
24
|
-
:items="brands"
|
|
23
|
+
v-model="brand as any"
|
|
24
|
+
:items="brands as any"
|
|
25
25
|
label-key="name"
|
|
26
26
|
:placeholder="$t('editor.email.generalStyle.selectBrand')"
|
|
27
27
|
class="w-full"
|
|
@@ -28,7 +28,7 @@ const tabs: TabsItem[] = [
|
|
|
28
28
|
{ label: t('common.mobile'), value: 'mobile' },
|
|
29
29
|
] as const
|
|
30
30
|
|
|
31
|
-
const currentView = ref<
|
|
31
|
+
const currentView = ref<'desktop' | 'mobile'>('desktop')
|
|
32
32
|
|
|
33
33
|
const width = computed(() => {
|
|
34
34
|
return currentView.value === 'mobile' ? '400px' : '100%'
|
|
@@ -35,7 +35,7 @@ const { mutate: preview, data: previewed, isLoading: isPreviewing } = usePreview
|
|
|
35
35
|
|
|
36
36
|
const previewEmail = useDebounceFn(() => {
|
|
37
37
|
preview({
|
|
38
|
-
message: props.message,
|
|
38
|
+
message: props.message as any,
|
|
39
39
|
parameters: parameterValues.value,
|
|
40
40
|
brand: props.brand
|
|
41
41
|
? {
|
|
@@ -107,7 +107,9 @@ const schema = z.object({
|
|
|
107
107
|
|
|
108
108
|
type Schema = z.output<typeof schema>
|
|
109
109
|
|
|
110
|
-
const resolvedBrandId = computed(
|
|
110
|
+
const resolvedBrandId = computed(
|
|
111
|
+
() => props.brandId ?? messageGroup.value?.brand_id ?? emailEditorStore.brandId,
|
|
112
|
+
)
|
|
111
113
|
|
|
112
114
|
function onSubmit() {
|
|
113
115
|
if (!resolvedMessage.value) return
|
|
@@ -74,10 +74,10 @@ const translatableBlocks = computed<BlockInfo[]>(() => {
|
|
|
74
74
|
})
|
|
75
75
|
.filter((block) => {
|
|
76
76
|
if (!block) return false
|
|
77
|
-
if (!translatorComponents[block.type]) return false
|
|
77
|
+
if (!translatorComponents[block.type as keyof typeof translatorComponents]) return false
|
|
78
78
|
if (filteredBlockIds && !filteredBlockIds.includes(block.baseBlock.id)) return false
|
|
79
79
|
return true
|
|
80
|
-
})
|
|
80
|
+
}) as BlockInfo[]
|
|
81
81
|
})
|
|
82
82
|
|
|
83
83
|
// Get block type display name
|
|
@@ -127,7 +127,7 @@ function getBlockTypeLabel(type: string): string {
|
|
|
127
127
|
<!-- Content row -->
|
|
128
128
|
<div class="px-4 py-3 show-list">
|
|
129
129
|
<component
|
|
130
|
-
:is="translatorComponents[info.type]"
|
|
130
|
+
:is="translatorComponents[info.type as keyof typeof translatorComponents]"
|
|
131
131
|
:base-block="info.baseBlock"
|
|
132
132
|
v-model="info.targetBlock"
|
|
133
133
|
/>
|
|
@@ -33,8 +33,10 @@ function handleCategoryChange(newCategory: string) {
|
|
|
33
33
|
t(`categories.${modelValue.value}`),
|
|
34
34
|
t(`categories.${newCategory}`),
|
|
35
35
|
]),
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
confirmProps: {
|
|
37
|
+
label: t('dialogs.whatsapp_change_category.change_to', [t(`categories.${newCategory}`)]),
|
|
38
|
+
},
|
|
39
|
+
cancelProps: { label: t('dialogs.whatsapp_change_category.cancel') },
|
|
38
40
|
action: () => {
|
|
39
41
|
modelValue.value = newCategory
|
|
40
42
|
selectedCategory.value = newCategory
|
|
@@ -19,7 +19,12 @@ import {
|
|
|
19
19
|
import { extractFromLocale } from '@/utils/locale'
|
|
20
20
|
import { computed, watch } from 'vue'
|
|
21
21
|
import { useI18n } from 'vue-i18n'
|
|
22
|
-
|
|
22
|
+
interface DropdownSectionAction {
|
|
23
|
+
value?: string
|
|
24
|
+
label: string
|
|
25
|
+
disabled?: boolean
|
|
26
|
+
onClick?: () => void
|
|
27
|
+
}
|
|
23
28
|
import PhoneInput from '@/components/Common/PhoneInput.vue'
|
|
24
29
|
import { z } from 'zod'
|
|
25
30
|
import {
|
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
WhatsappMessage,
|
|
6
6
|
WaButtonV1,
|
|
7
7
|
HeaderBlockV1,
|
|
8
|
+
ManagedLocales,
|
|
8
9
|
} from '@dev.smartpricing/message-composer-utils/types'
|
|
9
10
|
import WhatsappEditorHeader from './Header.vue'
|
|
10
11
|
import WhatsappEditorBody from './Body.vue'
|
|
@@ -80,7 +81,7 @@ const bodyGenerateContext = computed(() => {
|
|
|
80
81
|
<WhatsappEditorButtons
|
|
81
82
|
class="px-4"
|
|
82
83
|
v-model="model.body.buttons"
|
|
83
|
-
:locale="model.language_id"
|
|
84
|
+
:locale="model.language_id as ManagedLocales"
|
|
84
85
|
@button-added="emit('buttonAdded', $event)"
|
|
85
86
|
@button-removed="emit('buttonRemoved', $event)"
|
|
86
87
|
@button-type-changed="emit('buttonTypeChanged', $event)"
|
|
@@ -139,7 +139,7 @@ const linkButtonSchema = z.object({
|
|
|
139
139
|
<UForm
|
|
140
140
|
v-if="button.type === 'link'"
|
|
141
141
|
:schema="linkButtonSchema"
|
|
142
|
-
:state="targetButtons[index]"
|
|
142
|
+
:state="targetButtons[index] as unknown as Record<string, unknown>"
|
|
143
143
|
:validate-on="['blur']"
|
|
144
144
|
>
|
|
145
145
|
<UFormField
|
|
@@ -184,7 +184,7 @@ const linkButtonSchema = z.object({
|
|
|
184
184
|
<UForm
|
|
185
185
|
v-if="button.type === 'phone'"
|
|
186
186
|
:schema="phoneButtonSchema"
|
|
187
|
-
:state="targetButtons[index]"
|
|
187
|
+
:state="targetButtons[index] as unknown as Record<string, unknown>"
|
|
188
188
|
:validate-on="['blur']"
|
|
189
189
|
>
|
|
190
190
|
<UFormField
|
|
@@ -24,7 +24,9 @@ export function defineComposerContext(context: ComposerContext): void {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export function useComposerContext(): ComposerContext {
|
|
27
|
-
|
|
27
|
+
// runWithContext ensures inject() can reach app-level provides
|
|
28
|
+
// even outside component setup (e.g. Pinia stores, middleware).
|
|
29
|
+
const context = useNuxtApp().vueApp.runWithContext(() => inject(COMPOSER_CONTEXT_KEY))
|
|
28
30
|
if (!context) {
|
|
29
31
|
throw new Error(
|
|
30
32
|
'[message-composer] ComposerContext not provided. ' +
|
|
@@ -59,13 +59,14 @@ export function useAutoTranslateEmail() {
|
|
|
59
59
|
case 'paragraph':
|
|
60
60
|
case 'footer':
|
|
61
61
|
case 'unsubscribe_link': {
|
|
62
|
+
const target = targetBlock as typeof baseBlock
|
|
62
63
|
// Check if base has content
|
|
63
64
|
if (isTiptapEmpty(baseBlock.settings.content, 'email_v1')) {
|
|
64
65
|
return false
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
// If onlyEmpty, check if target already has content
|
|
68
|
-
if (onlyEmpty && !isTiptapEmpty(
|
|
69
|
+
if (onlyEmpty && !isTiptapEmpty(target.settings.content, 'email_v1')) {
|
|
69
70
|
return false
|
|
70
71
|
}
|
|
71
72
|
|
|
@@ -78,27 +79,29 @@ export function useAutoTranslateEmail() {
|
|
|
78
79
|
if (!translated) return false
|
|
79
80
|
|
|
80
81
|
// Update target block
|
|
81
|
-
|
|
82
|
+
target.settings.content = createTipTapContent(translated, { preset: 'email' })
|
|
82
83
|
return true
|
|
83
84
|
}
|
|
84
85
|
|
|
85
86
|
case 'discount': {
|
|
87
|
+
const target = targetBlock as typeof baseBlock
|
|
86
88
|
// Check if base has content
|
|
87
89
|
if (isTiptapEmpty(baseBlock.settings.content, 'email_v1')) {
|
|
88
90
|
return false
|
|
89
91
|
}
|
|
90
|
-
|
|
92
|
+
target.settings.content = deepClone(baseBlock.settings.content)
|
|
91
93
|
return true
|
|
92
94
|
}
|
|
93
95
|
|
|
94
96
|
case 'button': {
|
|
97
|
+
const target = targetBlock as typeof baseBlock
|
|
95
98
|
// Check if base has content
|
|
96
99
|
if (isTiptapEmpty(baseBlock.settings.content, 'email_v1')) {
|
|
97
100
|
return false
|
|
98
101
|
}
|
|
99
102
|
|
|
100
103
|
// If onlyEmpty, check if target already has content
|
|
101
|
-
if (onlyEmpty && !isTiptapEmpty(
|
|
104
|
+
if (onlyEmpty && !isTiptapEmpty(target.settings.content, 'email_v1')) {
|
|
102
105
|
return false
|
|
103
106
|
}
|
|
104
107
|
|
|
@@ -111,18 +114,19 @@ export function useAutoTranslateEmail() {
|
|
|
111
114
|
if (!translated) return false
|
|
112
115
|
|
|
113
116
|
// Update target block content only
|
|
114
|
-
|
|
117
|
+
target.settings.content = createTipTapContent(translated, { preset: 'email' })
|
|
115
118
|
return true
|
|
116
119
|
}
|
|
117
120
|
|
|
118
121
|
case 'raw_html': {
|
|
122
|
+
const target = targetBlock as typeof baseBlock
|
|
119
123
|
const translated = await translateText(
|
|
120
124
|
baseBlock.settings.html,
|
|
121
125
|
baseLanguage,
|
|
122
126
|
targetLanguage,
|
|
123
127
|
)
|
|
124
128
|
if (!translated) return false
|
|
125
|
-
|
|
129
|
+
target.settings.html = translated
|
|
126
130
|
return true
|
|
127
131
|
}
|
|
128
132
|
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { ref, computed, watch, toRaw, type MaybeRef } from 'vue'
|
|
2
2
|
import { deepClone, deepEqual } from '@dev.smartpricing/message-composer-utils/utils'
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
EmailMessage,
|
|
5
|
+
EmailBlock,
|
|
6
|
+
PostEntity,
|
|
7
|
+
} from '@dev.smartpricing/message-composer-utils/types'
|
|
4
8
|
|
|
5
9
|
/**
|
|
6
10
|
* Storage key pattern for email change tracking
|
|
@@ -117,7 +121,10 @@ export function useChangeTrackingEmail(messageGroupId: MaybeRef<string | number>
|
|
|
117
121
|
* @param currentMessage - Current email message to compare
|
|
118
122
|
* @returns Array of block IDs that have changed
|
|
119
123
|
*/
|
|
120
|
-
function getChangedBlockIds(
|
|
124
|
+
function getChangedBlockIds(
|
|
125
|
+
language: string,
|
|
126
|
+
currentMessage: EmailMessage | PostEntity<EmailMessage>,
|
|
127
|
+
): string[] {
|
|
121
128
|
const original = originalStates.value[language]
|
|
122
129
|
if (!original) return []
|
|
123
130
|
|
|
@@ -149,7 +156,10 @@ export function useChangeTrackingEmail(messageGroupId: MaybeRef<string | number>
|
|
|
149
156
|
* @param currentMessage - Current email message to compare
|
|
150
157
|
* @returns true if subject changed
|
|
151
158
|
*/
|
|
152
|
-
function hasSubjectChanged(
|
|
159
|
+
function hasSubjectChanged(
|
|
160
|
+
language: string,
|
|
161
|
+
currentMessage: EmailMessage | PostEntity<EmailMessage>,
|
|
162
|
+
): boolean {
|
|
153
163
|
const original = originalStates.value[language]
|
|
154
164
|
if (!original) return false
|
|
155
165
|
return original.subject !== currentMessage.body.subject
|
|
@@ -20,8 +20,8 @@ export function useExitConfirmation(shouldBlockNavigation: ComputedRef<boolean>)
|
|
|
20
20
|
confirm({
|
|
21
21
|
title: t('dialogs.unsaved_changes.title'),
|
|
22
22
|
message: t('dialogs.unsaved_changes.message'),
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
confirmProps: { label: t('dialogs.unsaved_changes.leave') },
|
|
24
|
+
cancelProps: { label: t('dialogs.unsaved_changes.cancel') },
|
|
25
25
|
action: () => {
|
|
26
26
|
const target = isNavigatingTo.value
|
|
27
27
|
if (target) {
|
|
@@ -18,7 +18,7 @@ export function useMediaLimits(mediaType: MaybeRefOrGetter<MediaType>) {
|
|
|
18
18
|
})
|
|
19
19
|
|
|
20
20
|
function validateFile(file: File): boolean {
|
|
21
|
-
if (!limits.value.accept.includes(file.type)) {
|
|
21
|
+
if (!(limits.value.accept as readonly string[]).includes(file.type)) {
|
|
22
22
|
useToast().add({
|
|
23
23
|
color: 'error',
|
|
24
24
|
title: t('common.imageUpload.mediaInvalidType'),
|
|
@@ -29,7 +29,12 @@ export function useTemplateFormValidation(options: UseTemplateFormValidationOpti
|
|
|
29
29
|
const { t } = useI18n()
|
|
30
30
|
const emailEditorStore = useEmailEditorStore()
|
|
31
31
|
|
|
32
|
-
const formRef = useTemplateRef
|
|
32
|
+
const formRef = useTemplateRef<{
|
|
33
|
+
clear: () => void
|
|
34
|
+
validate: () => Promise<void>
|
|
35
|
+
errors: { message: string }[]
|
|
36
|
+
setErrors: (errors: { message: string; name: string }[]) => void
|
|
37
|
+
}>('templateForm')
|
|
33
38
|
const validationErrors = ref<string[]>([])
|
|
34
39
|
|
|
35
40
|
const formState = computed(() => ({
|
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
Message,
|
|
13
13
|
PostEntity,
|
|
14
14
|
MessageStatus,
|
|
15
|
+
MessageGroup,
|
|
15
16
|
} from '@dev.smartpricing/message-composer-utils/types'
|
|
16
17
|
|
|
17
18
|
interface TemplateData<T = Message | PostEntity<Message>> {
|
|
@@ -55,7 +56,9 @@ export function useTemplateOperations(options: UseTemplateOperationsOptions = {}
|
|
|
55
56
|
async function createTemplate(data: TemplateData, status: MessageStatus = 'draft') {
|
|
56
57
|
saving.value = true
|
|
57
58
|
try {
|
|
58
|
-
const createdMessageGroup = await createMessageGroup(
|
|
59
|
+
const createdMessageGroup = await createMessageGroup(
|
|
60
|
+
data.messageGroup as PostEntity<MessageGroup>,
|
|
61
|
+
)
|
|
59
62
|
|
|
60
63
|
if (createdMessageGroup?.id) {
|
|
61
64
|
for (const message of Object.values(data.messages)) {
|
|
@@ -78,7 +78,10 @@ export function useTranslationFiltersEmail(params: EmailTranslationFiltersParams
|
|
|
78
78
|
/**
|
|
79
79
|
* Check if a block matches the search query
|
|
80
80
|
*/
|
|
81
|
-
function matchesSearch(
|
|
81
|
+
function matchesSearch(
|
|
82
|
+
blockId: string,
|
|
83
|
+
baseMsg: EmailMessage | PostEntity<EmailMessage>,
|
|
84
|
+
): boolean {
|
|
82
85
|
if (!searchQuery.value || !searchQuery.value.trim()) return true
|
|
83
86
|
|
|
84
87
|
const query = searchQuery.value.toLowerCase().trim()
|
|
@@ -93,7 +96,10 @@ export function useTranslationFiltersEmail(params: EmailTranslationFiltersParams
|
|
|
93
96
|
/**
|
|
94
97
|
* Check if a block has missing translation
|
|
95
98
|
*/
|
|
96
|
-
function hasMissingTranslation(
|
|
99
|
+
function hasMissingTranslation(
|
|
100
|
+
blockId: string,
|
|
101
|
+
targetMsg: EmailMessage | PostEntity<EmailMessage>,
|
|
102
|
+
): boolean {
|
|
97
103
|
const block = findBlockById(targetMsg.body.blocks, blockId)
|
|
98
104
|
if (!block) return true
|
|
99
105
|
|
|
@@ -117,7 +123,7 @@ export function useTranslationFiltersEmail(params: EmailTranslationFiltersParams
|
|
|
117
123
|
/**
|
|
118
124
|
* Get all translatable block IDs from a message, including parent grid blocks
|
|
119
125
|
*/
|
|
120
|
-
function getAllTranslatableBlockIds(message: EmailMessage): string[] {
|
|
126
|
+
function getAllTranslatableBlockIds(message: EmailMessage | PostEntity<EmailMessage>): string[] {
|
|
121
127
|
const blockIds = new Set<string>()
|
|
122
128
|
|
|
123
129
|
function isTranslatableOrMedia(type: string): boolean {
|
|
@@ -39,11 +39,11 @@ const isEmail = computed(() => selectedMessage.value?.render_type === 'email_v1'
|
|
|
39
39
|
|
|
40
40
|
<template>
|
|
41
41
|
<div :data-testid="previewPageTestIds.page" class="w-full overflow-y-auto">
|
|
42
|
-
<WhatsappPreview v-if="isWhatsapp && selectedMessage" :message="selectedMessage" />
|
|
42
|
+
<WhatsappPreview v-if="isWhatsapp && selectedMessage" :message="selectedMessage as any" />
|
|
43
43
|
|
|
44
44
|
<EmailPreview
|
|
45
45
|
v-else-if="isEmail && selectedMessage"
|
|
46
|
-
:message="selectedMessage"
|
|
46
|
+
:message="selectedMessage as any"
|
|
47
47
|
:show-subject="route.query.showSubject === 'true'"
|
|
48
48
|
:message-group-id="route.params.id as string"
|
|
49
49
|
:brand-id="brandId"
|
package/app/queries/brands.ts
CHANGED
|
@@ -11,7 +11,7 @@ export function useBrandsQuery(options?: {
|
|
|
11
11
|
direction?: 'asc' | 'desc'
|
|
12
12
|
}) {
|
|
13
13
|
return useQuery({
|
|
14
|
-
key: () => ['brands', options?.sort_by, options?.direction],
|
|
14
|
+
key: () => ['brands', options?.sort_by ?? '', options?.direction ?? ''],
|
|
15
15
|
query: () => brandsApi.getAll(options),
|
|
16
16
|
staleTime: 2 * 60 * 1000,
|
|
17
17
|
})
|
|
@@ -153,10 +153,10 @@ export const useEmailEditorStore = defineStore('emailEditor', () => {
|
|
|
153
153
|
async function initBrand(brandIdToRestore?: string | null) {
|
|
154
154
|
const list = await fetchBrands({ sort_by: 'created_at', direction: 'asc' })
|
|
155
155
|
if (brandIdToRestore) {
|
|
156
|
-
brand.value = list.find((b) => b.id === brandIdToRestore) ?? null
|
|
156
|
+
brand.value = (list.find((b) => b.id === brandIdToRestore) ?? null) as BrandFromDb | null
|
|
157
157
|
} else if (brandIdToRestore === undefined && list.length) {
|
|
158
158
|
// Create mode: default to oldest brand
|
|
159
|
-
brand.value = list[0]
|
|
159
|
+
brand.value = (list[0] ?? null) as BrandFromDb | null
|
|
160
160
|
}
|
|
161
161
|
// brandIdToRestore === null → keep null (explicit no-brand)
|
|
162
162
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dev.smartpricing/message-composer-layer",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.18",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./nuxt.config.ts",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"vue-router": "^5.0.6",
|
|
40
40
|
"vue3-emoji-picker": "^1.1.8",
|
|
41
41
|
"zod": "^4.4.1",
|
|
42
|
-
"@dev.smartpricing/message-composer-utils": "3.1.
|
|
42
|
+
"@dev.smartpricing/message-composer-utils": "3.1.18"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@nuxt/eslint": "^1.15.2",
|
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
"dev": "nuxt dev",
|
|
58
58
|
"lint": "eslint app/ --fix",
|
|
59
59
|
"format": "prettier --write app/",
|
|
60
|
+
"typecheck": "nuxt typecheck",
|
|
60
61
|
"postinstall": "nuxt prepare",
|
|
61
62
|
"test:e2e": "playwright test",
|
|
62
63
|
"test:e2e:debug": "playwright test --debug",
|