@dev.smartpricing/message-composer-layer 1.0.17 → 1.0.19

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.
Files changed (33) hide show
  1. package/app/components/Brand/BrandPreview.vue +6 -3
  2. package/app/components/Brand/Steps/BrandStepVisual.vue +1 -0
  3. package/app/components/Common/LanguageList.vue +2 -2
  4. package/app/components/Common/PhoneInput.vue +1 -1
  5. package/app/components/Common/PreviewById.vue +2 -2
  6. package/app/components/Composer/MessageLibrary.vue +6 -6
  7. package/app/components/DynamicDataSlideover.vue +2 -2
  8. package/app/components/Editor/RichTextEditor.vue +2 -2
  9. package/app/components/Email/Editor/Blocks/ButtonBlock.vue +2 -2
  10. package/app/components/Email/Editor/Blocks/GridSettings.vue +2 -2
  11. package/app/components/Email/Editor/Blocks/HeaderSettings.vue +1 -1
  12. package/app/components/Email/Editor/Blocks/SpacerSettings.vue +1 -1
  13. package/app/components/Email/Editor/Index.vue +1 -1
  14. package/app/components/Email/InlinePreview.vue +1 -1
  15. package/app/components/Email/PreviewPanel.vue +1 -1
  16. package/app/components/Email/SendTestEmailModal.vue +3 -1
  17. package/app/components/Email/Translator/Index.vue +3 -3
  18. package/app/components/Whatsapp/CategoryChanger.vue +4 -2
  19. package/app/components/Whatsapp/Editor/Buttons.vue +6 -1
  20. package/app/components/Whatsapp/Editor/Index.vue +2 -1
  21. package/app/components/Whatsapp/Translator/Button.vue +2 -2
  22. package/app/composables/composerContext.ts +3 -1
  23. package/app/composables/useAutoTranslateEmail.ts +10 -6
  24. package/app/composables/useChangeTrackingEmail.ts +13 -3
  25. package/app/composables/useExitConfirmation.ts +2 -2
  26. package/app/composables/useMediaLimits.ts +1 -1
  27. package/app/composables/useTemplateFormValidation.ts +6 -1
  28. package/app/composables/useTemplateOperations.ts +4 -1
  29. package/app/composables/useTranslationFiltersEmail.ts +9 -3
  30. package/app/pages/preview/[id].vue +2 -2
  31. package/app/queries/brands.ts +1 -1
  32. package/app/stores/emailEditor.ts +4 -4
  33. 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
- ...emptyEmailBlock('heading'),
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
- ...emptyEmailBlock('paragraph'),
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
- ...emptyEmailBlock('button'),
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
- confirmLabel: t('common.actions.delete'),
197
- cancelLabel: t('common.actions.cancel'),
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
- confirmLabel: t('common.actions.delete'),
64
- cancelLabel: t('common.actions.cancel'),
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
- confirmLabel: t('common.actions.publish'),
85
- cancelLabel: t('common.actions.cancel'),
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
- confirmLabel: t('common.actions.unpublish'),
105
- cancelLabel: t('common.actions.cancel'),
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<CommandPaletteGroup<DynamicValueField>[]>(() => {
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
- confirmLabel: t('dialogs.grid_reduce_columns.confirm'),
40
- cancelLabel: t('common.actions.cancel'),
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 || undefined,
17
+ get: () => props.block.settings.src || emailEditorStore.brand?.logo_url || '',
18
18
  set: (value) => updateSettings({ src: value }),
19
19
  })
20
20
 
@@ -44,7 +44,7 @@ defineOptions({
44
44
  <USelect
45
45
  :items="sizePresetItems"
46
46
  :disabled="block.settings.customSize"
47
- v-model="block.settings.height"
47
+ v-model="block.settings.height as any"
48
48
  :data-testid="spacerSettingsTestIds.sizePresetSelect"
49
49
  />
50
50
  </UFormField>
@@ -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<(typeof tabs)[number]['value']>('desktop')
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
  ? {
@@ -24,7 +24,7 @@ const tabs: TabsItem[] = [
24
24
  { label: t('common.mobile'), value: 'mobile' },
25
25
  ] as const
26
26
 
27
- const currentView = ref<(typeof tabs)[number]['value']>('desktop')
27
+ const currentView = ref<'desktop' | 'mobile'>('desktop')
28
28
  </script>
29
29
 
30
30
  <template>
@@ -107,7 +107,9 @@ const schema = z.object({
107
107
 
108
108
  type Schema = z.output<typeof schema>
109
109
 
110
- const resolvedBrandId = computed(() => props.brandId ?? messageGroup.value?.brand_id ?? emailEditorStore.brandId)
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
- confirmLabel: t('dialogs.whatsapp_change_category.change_to', [t(`categories.${newCategory}`)]),
37
- cancelLabel: t('dialogs.whatsapp_change_category.cancel'),
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
- import type { DropdownSectionAction } from '#ui/types'
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
- const context = inject(COMPOSER_CONTEXT_KEY)
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(targetBlock.settings.content, 'email_v1')) {
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
- targetBlock.settings.content = createTipTapContent(translated, { preset: 'email' })
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
- targetBlock.settings.content = deepClone(baseBlock.settings.content)
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(targetBlock.settings.content, 'email_v1')) {
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
- targetBlock.settings.content = createTipTapContent(translated, { preset: 'email' })
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
- targetBlock.settings.html = translated
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 { EmailMessage, EmailBlock } from '@dev.smartpricing/message-composer-utils/types'
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(language: string, currentMessage: EmailMessage): string[] {
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(language: string, currentMessage: EmailMessage): boolean {
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
- confirmLabel: t('dialogs.unsaved_changes.leave'),
24
- cancelLabel: t('dialogs.unsaved_changes.cancel'),
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('templateForm')
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(data.messageGroup)
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(blockId: string, baseMsg: EmailMessage): boolean {
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(blockId: string, targetMsg: EmailMessage): boolean {
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"
@@ -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
  })
@@ -13,10 +13,10 @@ import { brandsApi } from '~/api'
13
13
  export const useEmailEditorStore = defineStore('emailEditor', () => {
14
14
  // State
15
15
  const blocks = ref<EmailBlock[]>([])
16
- const selectedBlockId = ref<string | null>(null)
16
+ const selectedBlockId = ref<string>()
17
17
  const globalSettings = ref<EmailGlobalSettings>({})
18
18
  const brands = ref<BrandFromDb[]>([])
19
- const brand = ref<BrandFromDb | null>(null)
19
+ const brand = ref<BrandFromDb>()
20
20
 
21
21
  // Brand computed helpers
22
22
  const brandId = computed(() => brand.value?.id ?? null)
@@ -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.17",
3
+ "version": "1.0.19",
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.17"
42
+ "@dev.smartpricing/message-composer-utils": "3.1.19"
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",