@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.
Files changed (169) hide show
  1. package/README.md +75 -0
  2. package/app/api/ai.ts +28 -0
  3. package/app/api/brands.ts +54 -0
  4. package/app/api/client.ts +56 -0
  5. package/app/api/compilation.ts +34 -0
  6. package/app/api/dynamicValues.ts +11 -0
  7. package/app/api/index.ts +11 -0
  8. package/app/api/languages.ts +6 -0
  9. package/app/api/media.ts +26 -0
  10. package/app/api/messageGroups.ts +49 -0
  11. package/app/api/messages.ts +22 -0
  12. package/app/api/meta.ts +18 -0
  13. package/app/api/tracking.ts +9 -0
  14. package/app/components/Brand/BrandDrawer.vue +281 -0
  15. package/app/components/Brand/BrandPreview.vue +137 -0
  16. package/app/components/Brand/Steps/BrandStepGeneral.vue +37 -0
  17. package/app/components/Brand/Steps/BrandStepSocials.vue +48 -0
  18. package/app/components/Brand/Steps/BrandStepVisual.vue +214 -0
  19. package/app/components/Common/BrandColorSwatches.vue +37 -0
  20. package/app/components/Common/ColorInput.vue +144 -0
  21. package/app/components/Common/EmojiPickerButton.vue +35 -0
  22. package/app/components/Common/FontFamilySelect.vue +39 -0
  23. package/app/components/Common/HtmlPreviewIframe.vue +68 -0
  24. package/app/components/Common/IncompleteTranslationsDialog.vue +97 -0
  25. package/app/components/Common/LanguageList.vue +356 -0
  26. package/app/components/Common/LoadingOverlay.vue +12 -0
  27. package/app/components/Common/MediaUploadOrSelect.vue +299 -0
  28. package/app/components/Common/MessagePreviewSlideover.vue +88 -0
  29. package/app/components/Common/MonacoEditor.vue +110 -0
  30. package/app/components/Common/PhoneInput.vue +804 -0
  31. package/app/components/Common/SectionHeader.vue +26 -0
  32. package/app/components/Common/TranslationActionBar.vue +139 -0
  33. package/app/components/Common/ViewLessMore.vue +40 -0
  34. package/app/components/CreateTemplateButton.vue +59 -0
  35. package/app/components/DynamicDataButton.vue +75 -0
  36. package/app/components/DynamicDataSlideover.vue +149 -0
  37. package/app/components/Editor/AiGenerateSlideover.vue +292 -0
  38. package/app/components/Editor/EditorLinkPopover.vue +154 -0
  39. package/app/components/Editor/RichTextEditor.vue +495 -0
  40. package/app/components/Email/Editor/BlockLibrary.vue +89 -0
  41. package/app/components/Email/Editor/BlockSettings.vue +60 -0
  42. package/app/components/Email/Editor/Blocks/ButtonBlock.vue +117 -0
  43. package/app/components/Email/Editor/Blocks/ButtonSettings.vue +228 -0
  44. package/app/components/Email/Editor/Blocks/DiscountCodeBlock.vue +74 -0
  45. package/app/components/Email/Editor/Blocks/DiscountCodeSettings.vue +162 -0
  46. package/app/components/Email/Editor/Blocks/DividerBlock.vue +31 -0
  47. package/app/components/Email/Editor/Blocks/DividerSettings.vue +108 -0
  48. package/app/components/Email/Editor/Blocks/FooterBlock.vue +61 -0
  49. package/app/components/Email/Editor/Blocks/FooterSettings.vue +123 -0
  50. package/app/components/Email/Editor/Blocks/GridBlock.vue +128 -0
  51. package/app/components/Email/Editor/Blocks/GridSettings.vue +109 -0
  52. package/app/components/Email/Editor/Blocks/HTMLBlock.vue +31 -0
  53. package/app/components/Email/Editor/Blocks/HTMLSettings.vue +85 -0
  54. package/app/components/Email/Editor/Blocks/HeaderBlock.vue +53 -0
  55. package/app/components/Email/Editor/Blocks/HeaderSettings.vue +113 -0
  56. package/app/components/Email/Editor/Blocks/HeadingBlock.vue +61 -0
  57. package/app/components/Email/Editor/Blocks/HeadingSettings.vue +156 -0
  58. package/app/components/Email/Editor/Blocks/ImageBlock.vue +49 -0
  59. package/app/components/Email/Editor/Blocks/ImageSettings.vue +53 -0
  60. package/app/components/Email/Editor/Blocks/IndexBlock.vue +155 -0
  61. package/app/components/Email/Editor/Blocks/ParagraphBlock.vue +61 -0
  62. package/app/components/Email/Editor/Blocks/ParagraphSettings.vue +157 -0
  63. package/app/components/Email/Editor/Blocks/SocialsBlock.vue +63 -0
  64. package/app/components/Email/Editor/Blocks/SocialsSettings.vue +117 -0
  65. package/app/components/Email/Editor/Blocks/SpacerBlock.vue +37 -0
  66. package/app/components/Email/Editor/Blocks/SpacerSettings.vue +95 -0
  67. package/app/components/Email/Editor/Blocks/TopProductsBlock.vue +47 -0
  68. package/app/components/Email/Editor/Blocks/TopProductsSettings.vue +87 -0
  69. package/app/components/Email/Editor/Blocks/UnsubscribeLinkBlock.vue +61 -0
  70. package/app/components/Email/Editor/Blocks/UnsubscribeLinkSettings.vue +123 -0
  71. package/app/components/Email/Editor/Canvas.vue +143 -0
  72. package/app/components/Email/Editor/GeneralStyleSettings.vue +125 -0
  73. package/app/components/Email/Editor/Index.vue +77 -0
  74. package/app/components/Email/Editor/LeftSidebar.vue +95 -0
  75. package/app/components/Email/InlinePreview.vue +124 -0
  76. package/app/components/Email/Preview.vue +126 -0
  77. package/app/components/Email/PreviewPanel.vue +57 -0
  78. package/app/components/Email/SendTestEmailModal.vue +121 -0
  79. package/app/components/Email/TranslationEditor.vue +198 -0
  80. package/app/components/Email/Translator/Button.vue +101 -0
  81. package/app/components/Email/Translator/Discount.vue +60 -0
  82. package/app/components/Email/Translator/Footer.vue +66 -0
  83. package/app/components/Email/Translator/Grid.vue +127 -0
  84. package/app/components/Email/Translator/Header.vue +90 -0
  85. package/app/components/Email/Translator/Heading.vue +66 -0
  86. package/app/components/Email/Translator/Image.vue +91 -0
  87. package/app/components/Email/Translator/Index.vue +138 -0
  88. package/app/components/Email/Translator/Paragraph.vue +66 -0
  89. package/app/components/Email/Translator/RawHtml.vue +64 -0
  90. package/app/components/Email/Translator/UnsubscribeLink.vue +59 -0
  91. package/app/components/TemplateList/BaseTable.vue +354 -0
  92. package/app/components/TemplateList/StatusCell.vue +67 -0
  93. package/app/components/Whatsapp/CategoryChanger.vue +63 -0
  94. package/app/components/Whatsapp/Editor/Body.vue +53 -0
  95. package/app/components/Whatsapp/Editor/Buttons.vue +341 -0
  96. package/app/components/Whatsapp/Editor/Footer.vue +70 -0
  97. package/app/components/Whatsapp/Editor/Header.vue +156 -0
  98. package/app/components/Whatsapp/Editor/Index.vue +89 -0
  99. package/app/components/Whatsapp/Editor/SyncWrapper.vue +166 -0
  100. package/app/components/Whatsapp/Preview.vue +197 -0
  101. package/app/components/Whatsapp/PreviewPanel.vue +24 -0
  102. package/app/components/Whatsapp/TranslationEditor.vue +150 -0
  103. package/app/components/Whatsapp/Translator/Body.vue +61 -0
  104. package/app/components/Whatsapp/Translator/Button.vue +209 -0
  105. package/app/components/Whatsapp/Translator/Footer.vue +56 -0
  106. package/app/components/Whatsapp/Translator/Header.vue +61 -0
  107. package/app/components/Whatsapp/Translator/HeaderMedia.vue +117 -0
  108. package/app/components/Whatsapp/Translator/Index.vue +159 -0
  109. package/app/composables/composerContext.ts +35 -0
  110. package/app/composables/useAutoTranslate.ts +242 -0
  111. package/app/composables/useAutoTranslateEmail.ts +294 -0
  112. package/app/composables/useChangeTracking.ts +212 -0
  113. package/app/composables/useChangeTrackingEmail.ts +227 -0
  114. package/app/composables/useChannels.ts +36 -0
  115. package/app/composables/useEmailBlockContext.ts +43 -0
  116. package/app/composables/useExitConfirmation.ts +39 -0
  117. package/app/composables/useGoBack.ts +15 -0
  118. package/app/composables/useIncompleteTranslations.ts +55 -0
  119. package/app/composables/useIncompleteTranslationsEmail.ts +56 -0
  120. package/app/composables/useMediaLimits.ts +42 -0
  121. package/app/composables/useTemplateFormValidation.ts +109 -0
  122. package/app/composables/useTemplateOperations.ts +147 -0
  123. package/app/composables/useTracking.ts +39 -0
  124. package/app/composables/useTranslationFilters.ts +176 -0
  125. package/app/composables/useTranslationFiltersEmail.ts +244 -0
  126. package/app/composables/useWhatsappTemplateBehaviors.ts +58 -0
  127. package/app/middleware/channel-guard.ts +13 -0
  128. package/app/pages/brands.vue +195 -0
  129. package/app/pages/images.vue +141 -0
  130. package/app/pages/index.vue +198 -0
  131. package/app/pages/preview/[id].vue +47 -0
  132. package/app/pages/template/email/[id].vue +596 -0
  133. package/app/pages/template/email/create.vue +562 -0
  134. package/app/pages/template/whatsapp/[id].vue +561 -0
  135. package/app/pages/template/whatsapp/create.vue +468 -0
  136. package/app/plugins/admin-shortcut.client.ts +7 -0
  137. package/app/plugins/tracking.client.ts +12 -0
  138. package/app/queries/ai.ts +20 -0
  139. package/app/queries/brands.ts +75 -0
  140. package/app/queries/compilation.ts +37 -0
  141. package/app/queries/dynamicValues.ts +13 -0
  142. package/app/queries/index.ts +9 -0
  143. package/app/queries/languages.ts +10 -0
  144. package/app/queries/media.ts +27 -0
  145. package/app/queries/messageGroups.ts +112 -0
  146. package/app/queries/messages.ts +42 -0
  147. package/app/queries/meta.ts +13 -0
  148. package/app/stores/adminStore.ts +50 -0
  149. package/app/stores/appContext.ts +99 -0
  150. package/app/stores/emailEditor.ts +220 -0
  151. package/app/stores/main.ts +3 -0
  152. package/app/stores/metaSubmission.ts +116 -0
  153. package/app/types/app-config.d.ts +14 -0
  154. package/app/types/hooks.d.ts +9 -0
  155. package/app/types/tracking.ts +148 -0
  156. package/app/utils/locale.ts +17 -0
  157. package/app/utils/notifyParent.ts +7 -0
  158. package/app/utils/resolveDefaultLanguage.ts +14 -0
  159. package/app/utils/socialDefaults.ts +42 -0
  160. package/app/utils/testIds.const.ts +728 -0
  161. package/app.config.ts +5 -0
  162. package/i18n/check-translations.ts +56 -0
  163. package/i18n/locales/de.ts +834 -0
  164. package/i18n/locales/en.ts +826 -0
  165. package/i18n/locales/es.ts +833 -0
  166. package/i18n/locales/fr.ts +837 -0
  167. package/i18n/locales/it.ts +831 -0
  168. package/nuxt.config.ts +65 -0
  169. package/package.json +64 -0
@@ -0,0 +1,64 @@
1
+ <script setup lang="ts">
2
+ import type { RawHtmlBlock } from '@dev.smartpricing/message-composer-utils/types'
3
+ import MonacoEditor from '@/components/Common/MonacoEditor.vue'
4
+ import HtmlPreviewIframe from '~/components/Common/HtmlPreviewIframe.vue'
5
+
6
+ defineOptions({
7
+ name: 'EmailTranslatorRawHtml',
8
+ })
9
+
10
+ const { baseBlock } = defineProps<{
11
+ baseBlock: RawHtmlBlock
12
+ }>()
13
+
14
+ const targetBlock = defineModel<RawHtmlBlock>({
15
+ required: true,
16
+ })
17
+
18
+ // Copy base HTML to target
19
+ function copyFromBase() {
20
+ targetBlock.value.settings.html = baseBlock.settings.html
21
+ }
22
+ </script>
23
+
24
+ <template>
25
+ <div class="flex gap-8 items-stretch" :data-testid="emailTranslatorRawHtmlTestIds.root">
26
+ <!-- Base language preview (read-only) -->
27
+ <div class="flex-1 min-w-0">
28
+ <div class="h-80 overflow-y-auto">
29
+ <HtmlPreviewIframe
30
+ :html="baseBlock.settings.html"
31
+ title="Html preview"
32
+ :data-testid="emailTranslatorRawHtmlTestIds.basePreview"
33
+ />
34
+ </div>
35
+ </div>
36
+
37
+ <!-- Translation editor (editable Monaco) -->
38
+ <div class="flex-1 min-w-0">
39
+ <div class="flex flex-col h-80">
40
+ <div class="flex-1 min-h-0">
41
+ <MonacoEditor
42
+ v-model="targetBlock.settings.html"
43
+ language="html"
44
+ :line-numbers="'on'"
45
+ :minimap="false"
46
+ :word-wrap="'on'"
47
+ :data-testid="emailTranslatorRawHtmlTestIds.monacoEditor"
48
+ />
49
+ </div>
50
+ <div class="flex justify-end pt-2">
51
+ <UButton
52
+ icon="i-lucide-copy"
53
+ variant="ghost"
54
+ size="xs"
55
+ @click="copyFromBase"
56
+ :label="$t('common.copy')"
57
+ :data-testid="emailTranslatorRawHtmlTestIds.copyFromBaseButton"
58
+ />
59
+ <slot name="actions" />
60
+ </div>
61
+ </div>
62
+ </div>
63
+ </div>
64
+ </template>
@@ -0,0 +1,59 @@
1
+ <script setup lang="ts">
2
+ import type { JSONContent } from '@tiptap/vue-3'
3
+ import { generateHTML } from '@tiptap/vue-3'
4
+ import { getTipTapExtensions } from '@dev.smartpricing/message-composer-utils/utils'
5
+ import RichTextEditor from '@/components/Editor/RichTextEditor.vue'
6
+ import { useAppContextStore } from '@/stores/appContext'
7
+ import type { UnsubscribeLinkBlock } from '@dev.smartpricing/message-composer-utils/types'
8
+
9
+ defineOptions({
10
+ name: 'EmailTranslatorUnsubscribeLink',
11
+ })
12
+
13
+ const { baseBlock } = defineProps<{
14
+ baseBlock: UnsubscribeLinkBlock
15
+ }>()
16
+
17
+ const targetBlock = defineModel<UnsubscribeLinkBlock>({
18
+ required: true,
19
+ })
20
+
21
+ const appContext = useAppContextStore()
22
+
23
+ function generatePreviewHtml(content: JSONContent | undefined): string {
24
+ if (!content) return ''
25
+ return generateHTML(content, getTipTapExtensions({ preset: 'email' })).replaceAll(
26
+ '<p></p>',
27
+ '<p>&nbsp;</p>',
28
+ )
29
+ }
30
+ </script>
31
+
32
+ <template>
33
+ <div class="flex gap-8 items-start" :data-testid="emailTranslatorUnsubscribeLinkTestIds.root">
34
+ <div class="flex-1 min-w-0">
35
+ <div
36
+ class="paragraph-sm text-primary-700 break-words"
37
+ v-html="generatePreviewHtml(baseBlock.settings.content)"
38
+ :data-testid="emailTranslatorUnsubscribeLinkTestIds.basePreview"
39
+ />
40
+ </div>
41
+
42
+ <div class="flex-1 min-w-0 flex gap-2 items-start">
43
+ <div class="flex-1">
44
+ <RichTextEditor
45
+ v-model:text="targetBlock.settings.content"
46
+ preset="email"
47
+ bold
48
+ italic
49
+ underline
50
+ link
51
+ dynamic-values
52
+ :data-testid="emailTranslatorUnsubscribeLinkTestIds.contentEditor"
53
+ />
54
+ </div>
55
+
56
+ <slot name="actions" />
57
+ </div>
58
+ </div>
59
+ </template>
@@ -0,0 +1,354 @@
1
+ <script setup lang="ts">
2
+ import {
3
+ MESSAGE_GROUP_CATEGORIES,
4
+ MESSAGE_STATUS,
5
+ type MessageGroupWithMessageSummary,
6
+ type RenderType,
7
+ } from '@dev.smartpricing/message-composer-utils/types'
8
+ import StatusCell from '@/components/TemplateList/StatusCell.vue'
9
+ import { useI18n } from 'vue-i18n'
10
+ import { ref, watch } from 'vue'
11
+ import { formatDate } from '@dev.smartpricing/message-composer-utils/utils'
12
+ import type { TableColumn } from '#ui/types'
13
+ import type { TrackingEventMap } from '@/types/tracking'
14
+
15
+ const emit = defineEmits<{
16
+ edit: [MessageGroupWithMessageSummary]
17
+ delete: [MessageGroupWithMessageSummary]
18
+ duplicate: [MessageGroupWithMessageSummary]
19
+ publish: [MessageGroupWithMessageSummary]
20
+ unpublish: [MessageGroupWithMessageSummary]
21
+ }>()
22
+
23
+ const adminStore = useAdminStore()
24
+ const { trackEvent } = useTracking()
25
+
26
+ const { t, locale } = useI18n()
27
+
28
+ function getLocalizedMessage(row: MessageGroupWithMessageSummary) {
29
+ // Try current locale
30
+ let message = row.messages.find((m) => m.language === locale.value)
31
+
32
+ // Try English
33
+ if (!message) {
34
+ message = row.messages.find((m) => m.language === 'en')
35
+ }
36
+
37
+ // Use first message
38
+ if (!message) {
39
+ message = row.messages.at(0)
40
+ }
41
+
42
+ return message
43
+ }
44
+
45
+ function rowActions(row: MessageGroupWithMessageSummary) {
46
+ const publishAction =
47
+ adminStore.isAdminModeActive && row.visibility === 'internal'
48
+ ? {
49
+ label: t('common.actions.publish'),
50
+ icon: 'ph:globe',
51
+ color: 'info' as const,
52
+ onSelect: () => emit('publish', row),
53
+ 'data-testid': `${templateListTableTestIds.rowPublishAction}-${row.id}`,
54
+ }
55
+ : adminStore.isAdminModeActive && row.visibility === 'public'
56
+ ? {
57
+ label: t('common.actions.unpublish'),
58
+ icon: 'ph:globe-x',
59
+ color: 'warning' as const,
60
+ onSelect: () => emit('unpublish', row),
61
+ 'data-testid': `${templateListTableTestIds.rowUnpublishAction}-${row.id}`,
62
+ }
63
+ : null
64
+
65
+ return [
66
+ {
67
+ label: t('common.actions.edit'),
68
+ icon: 'ph:pencil-simple-line',
69
+ onSelect: () => emit('edit', row),
70
+ 'data-testid': `${templateListTableTestIds.rowEditAction}-${row.id}`,
71
+ },
72
+ ...(publishAction ? [publishAction] : []),
73
+ {
74
+ label: t('common.actions.duplicate'),
75
+ icon: 'ph:copy',
76
+ onSelect: () => emit('duplicate', row),
77
+ 'data-testid': `${templateListTableTestIds.rowDuplicateAction}-${row.id}`,
78
+ },
79
+ {
80
+ label: t('common.actions.delete'),
81
+ icon: 'ph:trash',
82
+ color: 'error' as const,
83
+ onSelect: () => emit('delete', row),
84
+ 'data-testid': `${templateListTableTestIds.rowDeleteAction}-${row.id}`,
85
+ },
86
+ ]
87
+ }
88
+
89
+ const columns: TableColumn<MessageGroupWithMessageSummary>[] = [
90
+ {
91
+ id: 'name',
92
+ accessorKey: 'name',
93
+ header: (ctx) => getSortableHeader(ctx, t('pages.message_library.headers.name')),
94
+ },
95
+ {
96
+ id: 'category',
97
+ accessorKey: 'category',
98
+ header: (ctx) => getSortableHeader(ctx, t('pages.message_library.headers.category')),
99
+ filterFn: (row, columnId, filterValue) => {
100
+ if (filterValue === 'all') return true
101
+ return row.original.category.toLowerCase().includes(filterValue.toLowerCase())
102
+ },
103
+ },
104
+ {
105
+ id: 'preview',
106
+ accessorFn: (row) => getLocalizedMessage(row)?.preview,
107
+ header: (ctx) => getSortableHeader(ctx, t('pages.message_library.headers.preview')),
108
+ },
109
+ {
110
+ id: 'status',
111
+ accessorFn: (row) => row.messages.map((m) => m.status).join(', '),
112
+ header: (ctx) => getSortableHeader(ctx, t('pages.message_library.headers.status')),
113
+ filterFn: (row, columnId, filterValue) => {
114
+ if (filterValue === 'all') return true
115
+ return row.original.messages.some((m) => m.status === filterValue)
116
+ },
117
+ },
118
+ // {
119
+ // id: 'tags',
120
+ // accessorFn: (row) => row.tags?.join(', ') || '',
121
+ // header: (ctx) => getSortableHeader(ctx, t('pages.message_library.headers.tags')),
122
+ // filterFn: (row, _columnId, filterValue: string[]) => {
123
+ // if (!filterValue || filterValue.length === 0) return true
124
+ // const rowTags = row.original.tags || []
125
+ // return filterValue.some((tag) => rowTags.includes(tag))
126
+ // },
127
+ // },
128
+ {
129
+ id: 'updated_at',
130
+ accessorFn: (row) => formatDate(row.updated_at),
131
+ // header: () => t('pages.message_library.headers.updated_at'),
132
+ header: (ctx) => getSortableHeader(ctx, t('pages.message_library.headers.updated_at')),
133
+ },
134
+ {
135
+ id: 'actions',
136
+ enableSorting: false,
137
+ header: '',
138
+ meta: {
139
+ class: {
140
+ th: 'w-[66px]',
141
+ td: 'w-[66px]',
142
+ },
143
+ },
144
+ },
145
+ ]
146
+
147
+ const sorting = ref([
148
+ {
149
+ id: 'updated_at',
150
+ desc: true,
151
+ },
152
+ ])
153
+
154
+ const props = defineProps<{
155
+ messageGroups: MessageGroupWithMessageSummary[]
156
+ loading: boolean
157
+ renderType: RenderType
158
+ }>()
159
+
160
+ const filteredMessageGroups = computed(() => {
161
+ if (adminStore.isAdminModeActive) return props.messageGroups
162
+ return props.messageGroups.filter((mg) => mg.visibility !== 'internal')
163
+ })
164
+
165
+ const categoryItems = computed(() => {
166
+ return [
167
+ {
168
+ label: t('common.all'),
169
+ value: 'all',
170
+ },
171
+ ...MESSAGE_GROUP_CATEGORIES.map((category) => ({
172
+ label: t(`categories.${category}`),
173
+ value: category,
174
+ })),
175
+ ]
176
+ })
177
+
178
+ const statusItems = computed(() => {
179
+ return [
180
+ {
181
+ label: t('common.all'),
182
+ value: 'all',
183
+ },
184
+
185
+ ...MESSAGE_STATUS.map((status) => ({
186
+ label: t(`status.${status}`),
187
+ value: status,
188
+ })),
189
+ ]
190
+ })
191
+
192
+ // const tagItems = computed(() => {
193
+ // const allTags = new Set<string>()
194
+ // messageGroups.forEach((mg) => mg.tags?.forEach((tag) => allTags.add(tag)))
195
+ // return [...allTags].sort()
196
+ // })
197
+
198
+ // const selectedTags = ref<string[]>([])
199
+
200
+ const table = useTemplateRef('table')
201
+
202
+ const messagesKind = computed(() => (props.renderType === 'email_v1' ? 'email' : 'whatsapp'))
203
+
204
+ watch(sorting, (val) => {
205
+ if (val[0]) {
206
+ trackEvent('table_sorting', {
207
+ messages_kind: messagesKind.value,
208
+ sort_column: val[0].id,
209
+ })
210
+ }
211
+ })
212
+
213
+ function trackFilters(overrides: Partial<TrackingEventMap['change_filters']> = {}) {
214
+ trackEvent('change_filters', {
215
+ search_term: (table.value?.tableApi?.getColumn('name')?.getFilterValue() as string) || '',
216
+ category: (table.value?.tableApi?.getColumn('category')?.getFilterValue() as string) || '',
217
+ status: (table.value?.tableApi?.getColumn('status')?.getFilterValue() as string) || '',
218
+ messages_kind: messagesKind.value,
219
+ ...overrides,
220
+ })
221
+ }
222
+
223
+ const trackSearchFilter = useDebounceFn((value: string) => {
224
+ trackFilters({ search_term: value })
225
+ }, 500)
226
+
227
+ function onSearchInput(value: string) {
228
+ table.value?.tableApi?.getColumn('name')?.setFilterValue(value)
229
+ trackSearchFilter(value)
230
+ }
231
+
232
+ function onCategoryFilter(value: string) {
233
+ table.value?.tableApi?.getColumn('category')?.setFilterValue(value)
234
+ trackFilters({ category: value })
235
+ }
236
+
237
+ function onStatusFilter(value: string) {
238
+ table.value?.tableApi?.getColumn('status')?.setFilterValue(value)
239
+ trackFilters({ status: value })
240
+ }
241
+ </script>
242
+
243
+ <template>
244
+ <div
245
+ class="flex-1 space-y-2.5 relative min-h-0 flex flex-col"
246
+ :data-testid="templateListTableTestIds.root"
247
+ >
248
+ <div class="flex gap-2">
249
+ <UInput
250
+ :model-value="table?.tableApi?.getColumn('name')?.getFilterValue() as string"
251
+ class="max-w-xs"
252
+ leading-icon="ph:magnifying-glass-bold"
253
+ :placeholder="t('pages.message_library.search_placeholder')"
254
+ :data-testid="templateListTableTestIds.searchInput"
255
+ @update:model-value="onSearchInput($event)"
256
+ />
257
+ <USelect
258
+ :model-value="table?.tableApi?.getColumn('category')?.getFilterValue() as string"
259
+ class="max-w-48"
260
+ :placeholder="t('pages.message_library.category_placeholder')"
261
+ :items="categoryItems"
262
+ :data-testid="templateListTableTestIds.categorySelect"
263
+ @update:model-value="onCategoryFilter($event)"
264
+ />
265
+ <USelect
266
+ :model-value="table?.tableApi?.getColumn('status')?.getFilterValue() as string"
267
+ class="max-w-48"
268
+ :placeholder="t('pages.message_library.status_placeholder')"
269
+ :items="statusItems"
270
+ :data-testid="templateListTableTestIds.statusSelect"
271
+ @update:model-value="onStatusFilter($event)"
272
+ />
273
+ <!-- <USelectMenu
274
+ v-if="tagItems.length > 0"
275
+ v-model="selectedTags"
276
+ :items="tagItems"
277
+ multiple
278
+ class="max-w-48"
279
+ :placeholder="t('pages.message_library.tags_placeholder')"
280
+ @update:model-value="table?.tableApi?.getColumn('tags')?.setFilterValue($event)"
281
+ /> -->
282
+ </div>
283
+
284
+ <UCard
285
+ :ui="{
286
+ root: 'flex-1 min-h-0 flex flex-col',
287
+ body: 'overflow-auto relative flex-1 p-0 sm:p-0 relative',
288
+ }"
289
+ :data-testid="templateListTableTestIds.card"
290
+ >
291
+ <UTable
292
+ :loading="loading"
293
+ class="flex-1 bg-white"
294
+ :ui="{
295
+ tr: 'hover:bg-muted',
296
+ root: 'overflow-visible static',
297
+ }"
298
+ :data="filteredMessageGroups"
299
+ :columns="columns"
300
+ sticky
301
+ ref="table"
302
+ v-model:sorting="sorting"
303
+ :data-testid="templateListTableTestIds.table"
304
+ >
305
+ <template #name-cell="{ row }">
306
+ <div
307
+ class="flex items-center gap-2"
308
+ :data-testid="`${templateListTableTestIds.rowName}-${row.original.id}`"
309
+ >
310
+ <span>{{ row.original.name }}</span>
311
+ <UBadge
312
+ v-if="row.original.visibility === 'internal'"
313
+ :label="t('common.internal')"
314
+ size="sm"
315
+ color="info"
316
+ variant="soft"
317
+ :data-testid="`${templateListTableTestIds.rowInternalBadge}-${row.original.id}`"
318
+ />
319
+ </div>
320
+ </template>
321
+
322
+ <template #category-cell="{ row }">
323
+ <UBadge
324
+ :label="row.original.category"
325
+ size="md"
326
+ color="neutral"
327
+ variant="soft"
328
+ :data-testid="`${templateListTableTestIds.rowCategoryBadge}-${row.original.id}`"
329
+ />
330
+ </template>
331
+
332
+ <template #status-cell="{ row }">
333
+ <StatusCell :items="row.original.messages" />
334
+ </template>
335
+
336
+ <template #preview-cell="{ row }">
337
+ <div
338
+ class="line-clamp-2 max-w-[200px] truncate"
339
+ :data-testid="`${templateListTableTestIds.rowPreview}-${row.original.id}`"
340
+ >
341
+ {{ getLocalizedMessage(row.original)?.preview }}
342
+ </div>
343
+ </template>
344
+
345
+ <template #actions-cell="{ row }">
346
+ <SMoreActions
347
+ :actions="rowActions(row.original)"
348
+ :data-testid="`${templateListTableTestIds.rowActions}-${row.original.id}`"
349
+ />
350
+ </template>
351
+ </UTable>
352
+ </UCard>
353
+ </div>
354
+ </template>
@@ -0,0 +1,67 @@
1
+ <script setup lang="ts">
2
+ import type {
3
+ MessageGroupWithMessageSummary,
4
+ MessageStatus,
5
+ } from '@dev.smartpricing/message-composer-utils/types'
6
+ import { computed } from 'vue'
7
+ import { useI18n } from 'vue-i18n'
8
+ import { groupBy } from '@dev.smartpricing/message-composer-utils/utils'
9
+ import { getLanguageDisplayName } from '@dev.smartpricing/message-composer-utils/utils'
10
+
11
+ const props = defineProps<{
12
+ items: MessageGroupWithMessageSummary['messages']
13
+ }>()
14
+
15
+ function getStatusColor(status: MessageStatus) {
16
+ switch (status) {
17
+ case 'draft':
18
+ return 'neutral'
19
+ case 'approved':
20
+ return 'success'
21
+ case 'rejected':
22
+ return 'error'
23
+ case 'pending':
24
+ return 'warning'
25
+ }
26
+ }
27
+
28
+ const statusOrder: MessageStatus[] = ['rejected', 'pending', 'approved', 'draft']
29
+
30
+ const { locale } = useI18n()
31
+
32
+ const grouped = computed(() => {
33
+ const groups = groupBy(props.items, (item) => item.status)
34
+ return statusOrder
35
+ .filter((status) => groups[status]?.length)
36
+ .map((status) => ({
37
+ status,
38
+ count: groups[status].length,
39
+ languages: groups[status].map((m) => getLanguageDisplayName(m.language, locale.value)),
40
+ }))
41
+ })
42
+ </script>
43
+
44
+ <template>
45
+ <div class="flex gap-2" :data-testid="templateListStatusCellTestIds.root">
46
+ <UTooltip
47
+ v-for="group in grouped"
48
+ :key="group.status"
49
+ :ui="{ content: 'h-fit' }"
50
+ :data-testid="`${templateListStatusCellTestIds.tooltip}-${group.status}`"
51
+ >
52
+ <UBadge
53
+ leading-icon="ph:circle-fill"
54
+ :label="`${group.count} ${$t(`status.${group.status}`)}`"
55
+ size="md"
56
+ :color="getStatusColor(group.status)"
57
+ variant="outline"
58
+ :data-testid="`${templateListStatusCellTestIds.badge}-${group.status}`"
59
+ />
60
+ <template #content>
61
+ <div :data-testid="`${templateListStatusCellTestIds.tooltipContent}-${group.status}`">
62
+ <p v-for="lang in group.languages" :key="lang">{{ lang }}</p>
63
+ </div>
64
+ </template>
65
+ </UTooltip>
66
+ </div>
67
+ </template>
@@ -0,0 +1,63 @@
1
+ <script setup lang="ts">
2
+ import { ref, computed } from 'vue'
3
+
4
+ import type { MessageGroupCategory } from '@dev.smartpricing/message-composer-utils/types'
5
+ import { MESSAGE_GROUP_CATEGORIES } from '@dev.smartpricing/message-composer-utils/types'
6
+ import { useI18n } from 'vue-i18n'
7
+
8
+ const modelValue = defineModel<MessageGroupCategory>('category', { required: true })
9
+
10
+ const selectedCategory = ref<MessageGroupCategory>(modelValue.value)
11
+
12
+ const { t } = useI18n()
13
+ const { confirm } = useConfirm()
14
+
15
+ const categoryItems = computed(() => {
16
+ return MESSAGE_GROUP_CATEGORIES.map((category) => ({
17
+ label: t(`categories.${category}`),
18
+ value: category,
19
+ }))
20
+ })
21
+
22
+ function assertIsMessageGroupCategory(newCategory: string): newCategory is MessageGroupCategory {
23
+ return MESSAGE_GROUP_CATEGORIES.includes(newCategory as MessageGroupCategory)
24
+ }
25
+
26
+ function handleCategoryChange(newCategory: string) {
27
+ if (!assertIsMessageGroupCategory(newCategory)) return
28
+ if (newCategory === modelValue.value) return
29
+
30
+ confirm({
31
+ title: t('dialogs.whatsapp_change_category.title'),
32
+ message: t('dialogs.whatsapp_change_category.message', [
33
+ t(`categories.${modelValue.value}`),
34
+ t(`categories.${newCategory}`),
35
+ ]),
36
+ confirmLabel: t('dialogs.whatsapp_change_category.change_to', [t(`categories.${newCategory}`)]),
37
+ cancelLabel: t('dialogs.whatsapp_change_category.cancel'),
38
+ action: () => {
39
+ modelValue.value = newCategory
40
+ selectedCategory.value = newCategory
41
+ emit('changed')
42
+ },
43
+ }).then((confirmed) => {
44
+ if (!confirmed) {
45
+ selectedCategory.value = modelValue.value
46
+ }
47
+ })
48
+ }
49
+
50
+ const emit = defineEmits<{
51
+ changed: []
52
+ }>()
53
+ </script>
54
+
55
+ <template>
56
+ <USelect
57
+ v-model="selectedCategory"
58
+ :placeholder="t('pages.whatsapp.create_message_group.category_placeholder')"
59
+ :items="categoryItems"
60
+ @update:model-value="handleCategoryChange"
61
+ :data-testid="categoryChangerTestIds.select"
62
+ />
63
+ </template>
@@ -0,0 +1,53 @@
1
+ <script lang="ts" setup>
2
+ import ViewLessMore from '@/components/Common/ViewLessMore.vue'
3
+ import RichTextEditor from '@/components/Editor/RichTextEditor.vue'
4
+ import {
5
+ WA_BODY_MAX_LENGTH,
6
+ type BodyBlockV1,
7
+ } from '@dev.smartpricing/message-composer-utils/types'
8
+ import { useAppContextStore } from '@/stores/appContext'
9
+
10
+ defineOptions({
11
+ name: 'WhatsappEditorBody',
12
+ })
13
+
14
+ const props = defineProps<{
15
+ generateContext?: string
16
+ }>()
17
+
18
+ const model = defineModel<BodyBlockV1>({ required: true })
19
+ const appContext = useAppContextStore()
20
+ </script>
21
+
22
+ <template>
23
+ <div :data-testid="whatsappEditorBodyTestIds.container">
24
+ <section class="max-w-[400px] pb-3">
25
+ <h4 class="label-md pb-3">{{ $t('editor.whatsapp.body.title') }}</h4>
26
+ <p
27
+ class="paragraph-sm pb-2 text-primary-800"
28
+ v-html="$t('editor.whatsapp.body.description_main')"
29
+ />
30
+ <ViewLessMore>
31
+ <ul class="list-disc list-inside text-sm space-y-2 text-primary-800 pb-2">
32
+ <li class="paragraph-sm" v-html="$t('editor.whatsapp.body.description_link')" />
33
+ <li class="paragraph-sm" v-html="$t('editor.whatsapp.body.description_dynamic_values')" />
34
+ </ul>
35
+ </ViewLessMore>
36
+ </section>
37
+
38
+ <RichTextEditor
39
+ v-model:text="model.content"
40
+ preset="whatsapp"
41
+ dynamic-values
42
+ bold
43
+ italic
44
+ emoji
45
+ improve
46
+ generate
47
+ generate-widget-type="whatsapp_body"
48
+ :generate-context="props.generateContext"
49
+ :character-limit="WA_BODY_MAX_LENGTH"
50
+ :data-testid="whatsappEditorBodyTestIds.contentEditor"
51
+ />
52
+ </div>
53
+ </template>