@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,26 @@
1
+ <script setup lang="ts">
2
+ defineProps<{
3
+ title: string
4
+ showReset?: boolean
5
+ }>()
6
+
7
+ const emit = defineEmits<{
8
+ reset: []
9
+ }>()
10
+ </script>
11
+
12
+ <template>
13
+ <div class="flex items-center justify-between" :data-testid="sectionHeaderTestIds.root">
14
+ <h4 class="paragraph-md-bold">{{ title }}</h4>
15
+ <UButton
16
+ v-if="showReset"
17
+ size="xs"
18
+ variant="outline"
19
+ color="primary"
20
+ icon="ph:arrow-counter-clockwise"
21
+ :label="$t('editor.email.common.reset')"
22
+ @click="emit('reset')"
23
+ :data-testid="sectionHeaderTestIds.resetButton"
24
+ />
25
+ </div>
26
+ </template>
@@ -0,0 +1,139 @@
1
+ <script setup lang="ts">
2
+ import { ref, watch } from 'vue'
3
+ import type { Message, PostEntity } from '@dev.smartpricing/message-composer-utils/types'
4
+ import { useDebounceFn } from '@vueuse/core'
5
+ import MessagePreviewSlideover from './MessagePreviewSlideover.vue'
6
+
7
+ interface Props {
8
+ messages: Record<string, PostEntity<Message>>
9
+ currentLanguage: string
10
+ baseLanguage: string
11
+ messageGroupId?: string | number
12
+ isTranslating?: boolean
13
+ searchQuery?: string
14
+ showOnlyMissing?: boolean
15
+ showOnlyChanges?: boolean
16
+ }
17
+
18
+ const props = withDefaults(defineProps<Props>(), {
19
+ searchQuery: '',
20
+ showOnlyMissing: false,
21
+ showOnlyChanges: false,
22
+ })
23
+
24
+ const emit = defineEmits<{
25
+ 'update:searchQuery': [query: string]
26
+ 'update:showOnlyMissing': [enabled: boolean]
27
+ 'update:showOnlyChanges': [enabled: boolean]
28
+ 'auto-translate': []
29
+ preview: []
30
+ }>()
31
+
32
+ // Local refs synced with props
33
+ const localSearchQuery = ref(props.searchQuery)
34
+ const localShowOnlyMissing = ref(props.showOnlyMissing)
35
+ const localShowOnlyChanges = ref(props.showOnlyChanges)
36
+
37
+ // Sync with props when they change
38
+ watch(
39
+ () => props.searchQuery,
40
+ (newValue) => {
41
+ localSearchQuery.value = newValue
42
+ },
43
+ )
44
+
45
+ watch(
46
+ () => props.showOnlyMissing,
47
+ (newValue) => {
48
+ localShowOnlyMissing.value = newValue
49
+ },
50
+ )
51
+
52
+ watch(
53
+ () => props.showOnlyChanges,
54
+ (newValue) => {
55
+ localShowOnlyChanges.value = newValue
56
+ },
57
+ )
58
+
59
+ const debouncedSearch = useDebounceFn((query: string) => {
60
+ emit('update:searchQuery', query)
61
+ }, 300)
62
+
63
+ watch(localSearchQuery, (newQuery) => {
64
+ debouncedSearch(newQuery)
65
+ })
66
+
67
+ watch(localShowOnlyMissing, (newValue) => {
68
+ emit('update:showOnlyMissing', newValue)
69
+ })
70
+
71
+ watch(localShowOnlyChanges, (newValue) => {
72
+ emit('update:showOnlyChanges', newValue)
73
+ })
74
+
75
+ const handleAutoTranslate = () => {
76
+ emit('auto-translate')
77
+ }
78
+ </script>
79
+
80
+ <template>
81
+ <div
82
+ class="sticky top-0 z-10 flex items-center gap-3 p-4 bg-white border-b border-muted"
83
+ :data-testid="translationActionBarTestIds.root"
84
+ >
85
+ <!-- Search Input -->
86
+ <UInput
87
+ v-model="localSearchQuery"
88
+ :placeholder="$t('translation_action_bar.search_placeholder')"
89
+ trailing-icon="ph:magnifying-glass"
90
+ size="sm"
91
+ class="w-60"
92
+ :data-testid="translationActionBarTestIds.searchInput"
93
+ >
94
+ </UInput>
95
+
96
+ <!-- Filter Toggles -->
97
+ <USwitch
98
+ v-model="localShowOnlyMissing"
99
+ :label="$t('translation_action_bar.show_missing')"
100
+ size="sm"
101
+ :data-testid="translationActionBarTestIds.showMissingSwitch"
102
+ />
103
+
104
+ <USwitch
105
+ v-model="localShowOnlyChanges"
106
+ :label="$t('translation_action_bar.show_changes')"
107
+ size="sm"
108
+ :data-testid="translationActionBarTestIds.showChangesSwitch"
109
+ />
110
+
111
+ <!-- Action Buttons -->
112
+ <UButton
113
+ size="sm"
114
+ icon="ph:translate"
115
+ square
116
+ color="ai"
117
+ variant="outline"
118
+ class="ml-auto"
119
+ @click="handleAutoTranslate"
120
+ :label="
121
+ isTranslating
122
+ ? $t('translation_action_bar.translating')
123
+ : $t('translation_action_bar.auto_translate_button')
124
+ "
125
+ :ui="{
126
+ label: 'hidden xl:block',
127
+ }"
128
+ :loading="isTranslating"
129
+ :disabled="isTranslating"
130
+ :data-testid="translationActionBarTestIds.autoTranslateButton"
131
+ />
132
+
133
+ <MessagePreviewSlideover
134
+ v-if="messages[currentLanguage]"
135
+ :message="messages[currentLanguage]"
136
+ @opened="emit('preview')"
137
+ />
138
+ </div>
139
+ </template>
@@ -0,0 +1,40 @@
1
+ <template>
2
+ <div :data-testid="viewLessMoreTestIds.root">
3
+ <div
4
+ class="overflow-clip duration-300"
5
+ style="transition-property: height"
6
+ :class="{
7
+ 'h-0': !open,
8
+ 'h-auto': open,
9
+ }"
10
+ >
11
+ <div v-show="open">
12
+ <slot />
13
+ </div>
14
+ </div>
15
+
16
+ <button
17
+ @click="toggle"
18
+ class="paragraph-xs text-secondary-700 flex gap-2 items-center"
19
+ :data-testid="viewLessMoreTestIds.toggleButton"
20
+ >
21
+ {{ open ? $t('common.view_less') : $t('common.view_more') }}
22
+ <UIcon :name="open ? 'i-lucide-chevron-up' : 'i-lucide-chevron-down'" class="size-4" />
23
+ </button>
24
+ </div>
25
+ </template>
26
+ <script setup lang="ts">
27
+ import { ref } from 'vue'
28
+
29
+ interface Props {}
30
+
31
+ defineProps<Props>()
32
+
33
+ const open = ref(false)
34
+
35
+ function toggle() {
36
+ open.value = !open.value
37
+ }
38
+ </script>
39
+
40
+ <style scoped></style>
@@ -0,0 +1,59 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import { useRouter } from 'vue-router'
4
+
5
+ const router = useRouter()
6
+ const { hasWhatsapp, hasEmail } = useChannels()
7
+ const { t } = useI18n()
8
+ const { trackEvent } = useTracking()
9
+
10
+ function createTemplate(type: 'whatsapp' | 'email') {
11
+ trackEvent('message_create', { message_kind: type })
12
+ router.push(`/template/${type}/create`)
13
+ }
14
+
15
+ const templateOptions = computed(() => {
16
+ const options = []
17
+ if (hasWhatsapp.value) {
18
+ options.push({
19
+ label: t('message.render_type.whatsapp_v1'),
20
+ onSelect: () => createTemplate('whatsapp'),
21
+ })
22
+ }
23
+ if (hasEmail.value) {
24
+ options.push({
25
+ label: t('message.render_type.email_v1'),
26
+ onSelect: () => createTemplate('email'),
27
+ })
28
+ }
29
+ return options
30
+ })
31
+
32
+ const singleChannelType = computed(() => {
33
+ if (hasWhatsapp.value && !hasEmail.value) return 'whatsapp'
34
+ if (hasEmail.value && !hasWhatsapp.value) return 'email'
35
+ return null
36
+ })
37
+ </script>
38
+
39
+ <template>
40
+ <UButton
41
+ v-if="singleChannelType"
42
+ :label="$t('pages.message_library.add_message_group')"
43
+ color="primary"
44
+ :data-testid="homeTestIds.createTemplateButton"
45
+ @click="createTemplate(singleChannelType)"
46
+ />
47
+ <UDropdownMenu
48
+ v-else
49
+ :items="templateOptions"
50
+ :popper="{ placement: 'bottom-end' }"
51
+ :data-testid="homeTestIds.createTemplateDropdown"
52
+ >
53
+ <UButton
54
+ :label="$t('pages.message_library.add_message_group')"
55
+ color="primary"
56
+ :data-testid="homeTestIds.createTemplateButton"
57
+ />
58
+ </UDropdownMenu>
59
+ </template>
@@ -0,0 +1,75 @@
1
+ <script lang="ts" setup>
2
+ import { ref } from 'vue'
3
+
4
+ interface DynamicValue {
5
+ value: string
6
+ label: string
7
+ }
8
+
9
+ const props = defineProps<{
10
+ values: DynamicValue[]
11
+ }>()
12
+
13
+ const emit = defineEmits<{
14
+ select: [data: DynamicValue]
15
+ }>()
16
+
17
+ const isOpen = ref(false)
18
+
19
+ function onSelectData(value: DynamicValue) {
20
+ emit('select', value)
21
+ isOpen.value = false
22
+ }
23
+ </script>
24
+
25
+ <template>
26
+ <UButton
27
+ icon="ph:brackets-curly-thin"
28
+ color="primary"
29
+ variant="ghost"
30
+ :data-testid="dynamicDataButtonTestIds.triggerButton"
31
+ @click="isOpen = true"
32
+ />
33
+
34
+ <UModal
35
+ v-model:open="isOpen"
36
+ :title="$t('modals.dynamic_data_modal.title')"
37
+ close-icon="ph:x"
38
+ :close="{ 'data-testid': dynamicDataButtonTestIds.modalCloseButton }"
39
+ :data-testid="dynamicDataButtonTestIds.modal"
40
+ >
41
+ <template #body>
42
+ <p class="text-sm text-primary-800 pb-4" :data-testid="dynamicDataButtonTestIds.description">
43
+ {{ $t('modals.dynamic_data_modal.description') }}
44
+ </p>
45
+
46
+ <p
47
+ class="text-xs font-bold text-primary-800 pb-2"
48
+ :data-testid="dynamicDataButtonTestIds.selectValueLabel"
49
+ >
50
+ {{ $t('modals.dynamic_data_modal.select_value') }}
51
+ </p>
52
+
53
+ <div
54
+ class="flex flex-col divide-y divide-gray-200 rounded-sm border border-gray-200"
55
+ :data-testid="dynamicDataButtonTestIds.valueList"
56
+ >
57
+ <div
58
+ v-for="(value, index) in props.values"
59
+ :key="index"
60
+ class="px-4 py-2.5 cursor-pointer hover:bg-primary-100"
61
+ :data-testid="`${dynamicDataButtonTestIds.valueItem}-${value.value}`"
62
+ @click="onSelectData(value)"
63
+ >
64
+ <p class="text-sm text-gray-900 pb-1">
65
+ {{ value.label }}
66
+ </p>
67
+ <p
68
+ class="text-xs text-primary-800"
69
+ v-html="$t('modals.dynamic_data_modal.will_display', { value: value.value })"
70
+ />
71
+ </div>
72
+ </div>
73
+ </template>
74
+ </UModal>
75
+ </template>
@@ -0,0 +1,149 @@
1
+ <script lang="ts" setup>
2
+ import type { ListboxItem, TabsItem } from '#ui/types'
3
+ import type { DynamicValueField } from '@dev.smartpricing/message-composer-utils/types'
4
+ import { useDynamicValuesQuery } from '~/queries'
5
+
6
+ const props = defineProps<{
7
+ defaultContext?: string
8
+ }>()
9
+
10
+ const emit = defineEmits<{
11
+ select: [data: { value: string; label: string }]
12
+ }>()
13
+
14
+ const isOpen = defineModel<boolean>('open', { default: false })
15
+
16
+ const { data, isLoading } = useDynamicValuesQuery()
17
+
18
+ const contextKeys = computed(() => (data.value ? Object.keys(data.value) : []))
19
+ const hasMultipleContexts = computed(() => contextKeys.value.length > 1)
20
+
21
+ const activeTab = ref<string | number>('')
22
+
23
+ // Pre-select tab when data loads
24
+ watch(
25
+ [data, () => props.defaultContext],
26
+ () => {
27
+ if (!data.value) return
28
+ if (props.defaultContext && contextKeys.value.includes(props.defaultContext)) {
29
+ activeTab.value = props.defaultContext
30
+ } else if (contextKeys.value.length > 0) {
31
+ activeTab.value = contextKeys.value[0]
32
+ }
33
+ },
34
+ { immediate: true },
35
+ )
36
+
37
+ const { t } = useI18n()
38
+
39
+ const tabItems = computed<TabsItem[]>(() =>
40
+ contextKeys.value.map((key) => ({
41
+ label: t(`modals.dynamic_data_modal.tabs.${key}`),
42
+ value: key,
43
+ })),
44
+ )
45
+
46
+ const activeContextKey = computed(() => {
47
+ const tab = String(activeTab.value)
48
+ return contextKeys.value.includes(tab) ? tab : contextKeys.value[0]
49
+ })
50
+
51
+ const contextDescriptions = computed(() => {
52
+ if (!data.value) return []
53
+ return contextKeys.value
54
+ .map((key) => ({
55
+ label: t(`modals.dynamic_data_modal.tabs.${key}`),
56
+ description: data.value![key]?.description || '',
57
+ }))
58
+ .filter((ctx) => ctx.description)
59
+ })
60
+
61
+ const commandGroups = computed<CommandPaletteGroup<DynamicValueField>[]>(() => {
62
+ if (!data.value || !activeContextKey.value) return []
63
+
64
+ const fields = data.value[activeContextKey.value]?.fields || []
65
+ return [
66
+ {
67
+ id: activeContextKey.value,
68
+ items: fields.map((field) => ({
69
+ ...field,
70
+ onSelect: () => {
71
+ emit('select', { value: field.value, label: field.value })
72
+ isOpen.value = false
73
+ },
74
+ })),
75
+ },
76
+ ]
77
+ })
78
+ </script>
79
+ <template>
80
+ <USlideover
81
+ v-model:open="isOpen"
82
+ :title="$t('modals.dynamic_data_modal.title')"
83
+ side="right"
84
+ :close="{ 'data-testid': dynamicDataSlideoverTestIds.closeButton }"
85
+ :ui="{ content: 'max-w-2xl', body: 'sm:p-0 p-0 flex flex-col', footer: 'justify-end' }"
86
+ :data-testid="dynamicDataSlideoverTestIds.slideover"
87
+ >
88
+ <UButton icon="ph:brackets-curly-thin" color="primary" variant="ghost" size="sm" />
89
+
90
+ <template #actions>
91
+ <UPopover>
92
+ <UButton
93
+ icon="i-lucide-info"
94
+ variant="ghost"
95
+ size="sm"
96
+ color="neutral"
97
+ :data-testid="dynamicDataSlideoverTestIds.infoButton"
98
+ />
99
+ <template #content>
100
+ <div class="p-4 max-w-xs space-y-3">
101
+ <p class="text-sm text-default">{{ $t('modals.dynamic_data_modal.info') }}</p>
102
+ <dl v-if="contextDescriptions.length > 0" class="space-y-2">
103
+ <div v-for="ctx in contextDescriptions" :key="ctx.label">
104
+ <dt class="text-sm font-medium text-default">{{ ctx.label }}</dt>
105
+ <dd class="text-sm text-muted">{{ ctx.description }}</dd>
106
+ </div>
107
+ </dl>
108
+ </div>
109
+ </template>
110
+ </UPopover>
111
+ </template>
112
+
113
+ <template #body>
114
+ <div v-if="isLoading" class="flex items-center justify-center py-8">
115
+ <UIcon name="i-lucide-loader-circle" class="size-6 animate-spin text-muted" />
116
+ </div>
117
+
118
+ <template v-else-if="data && contextKeys.length > 0">
119
+ <UTabs
120
+ v-if="hasMultipleContexts"
121
+ v-model="activeTab"
122
+ :items="tabItems"
123
+ :content="false"
124
+ variant="link"
125
+ :data-testid="dynamicDataSlideoverTestIds.tabs"
126
+ />
127
+ <UCommandPalette
128
+ :groups="commandGroups"
129
+ :placeholder="$t('modals.dynamic_data_modal.search_placeholder')"
130
+ :fuse="{
131
+ resultLimit: 100,
132
+ fuseOptions: {
133
+ keys: [
134
+ { name: 'label', weight: 2 },
135
+ { name: 'description', weight: 1 },
136
+ ],
137
+ },
138
+ }"
139
+ class="flex-1"
140
+ :data-testid="dynamicDataSlideoverTestIds.listbox"
141
+ />
142
+ </template>
143
+
144
+ <div v-else class="text-sm text-muted py-8 text-center">
145
+ {{ $t('modals.dynamic_data_modal.no_values') }}
146
+ </div>
147
+ </template>
148
+ </USlideover>
149
+ </template>