@dev.smartpricing/message-composer-layer 4.3.2-chat.0 → 4.3.2-fix.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/app/api/client.ts CHANGED
@@ -15,7 +15,9 @@ function assertAppError(data: unknown): data is AppError {
15
15
  export const apiClient = createApiClient({
16
16
  baseURL: useRuntimeConfig().public.COMPOSER_API_URL as string,
17
17
  csrf: {
18
- cookieName: (useRuntimeConfig().public.ENV_CSRF_COOKIE_NAME as string) || 'smt_csrf',
18
+ // Same key and derivation as platform-layer's own client: the host sets the
19
+ // full cookie name (env-suffixed: smt_csrf / smt_csrf_stage / smt_csrf_dev).
20
+ cookieName: (useRuntimeConfig().public.CSRF_COOKIE_NAME as string) || 'smt_csrf',
19
21
  },
20
22
  auth: {
21
23
  refreshBaseURL:
@@ -1,11 +1,8 @@
1
- import type {
2
- DynamicValuesResponse,
3
- MessageGroupProduct,
4
- } from '@dev.smartpricing/message-composer-utils/types'
1
+ import type { DynamicValuesResponse } from '@dev.smartpricing/message-composer-utils/types'
5
2
  import { apiClient } from './client'
6
3
 
7
4
  export const dynamicValuesApi = {
8
- get: (product: MessageGroupProduct, language: string) => {
5
+ get: (product: string, language: string) => {
9
6
  const params = new URLSearchParams()
10
7
  params.set('product', product)
11
8
  params.set('language', language)
@@ -1,38 +1,27 @@
1
1
  import type {
2
2
  MessageGroupFromDb,
3
- PaginatedResponse,
4
3
  MessageGroupWithMessages,
5
4
  MessageGroupWithMessageSummary,
6
5
  MessageGroupValidationResponse,
7
6
  PostEntity,
8
7
  MessageGroup,
8
+ RenderType,
9
9
  DuplicateMessageGroupBody,
10
- ListMessageGroupsQuery,
11
10
  } from '@dev.smartpricing/message-composer-utils/types'
12
11
  import { apiClient } from './client'
13
12
 
14
- /**
15
- * Query params of `GET /message-groups`, straight from the shared schema
16
- * (ADR 0007): snake_case, as on the wire. `visibility` other than `public`
17
- * is admin-only (403 otherwise); every field is optional here, the server
18
- * applies the defaults.
19
- */
20
- export type ListMessageGroupsOptions = Partial<ListMessageGroupsQuery>
21
-
22
- function listQueryString(options: ListMessageGroupsOptions = {}): string {
23
- const params = new URLSearchParams()
24
- for (const [name, value] of Object.entries(options)) {
25
- if (value !== undefined && value !== '') params.set(name, String(value))
26
- }
27
- return params.size ? `?${params.toString()}` : ''
28
- }
29
-
30
13
  export const messageGroupsApi = {
31
- /** One page of the server-side filtered list, in the Laravel envelope (ADR 0012). */
32
- list: (options?: ListMessageGroupsOptions) =>
33
- apiClient<PaginatedResponse<MessageGroupWithMessageSummary>>(
34
- `/message-groups${listQueryString(options)}`,
35
- ),
14
+ getAll: (options?: { renderType?: RenderType; tags?: string[]; tagsMode?: 'and' | 'or' }) => {
15
+ const params = new URLSearchParams()
16
+ if (options?.renderType) params.set('render_type', options.renderType)
17
+ if (options?.tags && options.tags.length > 0) {
18
+ options.tags.forEach((tag) => params.append('tags', tag))
19
+ }
20
+ if (options?.tagsMode) params.set('tags_mode', options.tagsMode)
21
+
22
+ const query = params.size ? `?${params.toString()}` : ''
23
+ return apiClient<MessageGroupWithMessageSummary[]>(`/message-groups${query}`)
24
+ },
36
25
 
37
26
  getById: (id: string) => apiClient<MessageGroupWithMessages>(`/message-groups/${id}`),
38
27
 
@@ -1,18 +1,9 @@
1
1
  <script setup lang="ts">
2
- import type { MessageStatus, RenderType } from '@dev.smartpricing/message-composer-utils/types'
3
- import { usePagedMessageGroups } from '~/composables/usePagedMessageGroups'
4
- import { useMessageGroupQuery } from '~/queries/messageGroups'
5
- import { useAppContextStore } from '~/stores/appContext'
2
+ import type { RenderType, MessageStatus } from '@dev.smartpricing/message-composer-utils/types'
3
+ import { useMessageGroupsQuery } from '~/queries/messageGroups'
6
4
 
7
5
  defineOptions({ name: 'MessageGroupSelect' })
8
6
 
9
- /**
10
- * In-composer Message Group picker (ADR 0012). Always lists the context
11
- * Product's groups only: search is server-side, debounced and bound to the
12
- * menu's search input; pages of 100 load on demand through "Load more".
13
- * The selected group stays in the menu even when the loaded pages no longer
14
- * contain it (search cleared, paging reset, or preselected beyond page one).
15
- */
16
7
  const props = withDefaults(
17
8
  defineProps<{
18
9
  renderType?: RenderType
@@ -43,88 +34,11 @@ const emit = defineEmits<{
43
34
  select: [item: { id: string; name: string }]
44
35
  }>()
45
36
 
46
- /** What the picker needs from a group, whichever endpoint it came from. */
47
- interface PickerGroup {
48
- id: string
49
- name: string
50
- messages: Array<{ status: MessageStatus }>
51
- }
52
-
53
- const PER_PAGE = 100
54
-
55
37
  const { t } = useI18n()
56
- const appContext = useAppContextStore()
57
38
 
58
- // Server-side search: the menu's own client filter is off (`ignore-filter`),
59
- // the typed term reaches the list request once it settles. A new search,
60
- // render type or context Product starts over from page one.
61
- const searchTerm = ref('')
62
- const page = ref(1)
63
-
64
- // Without a context Product the server would answer with every Product's
65
- // groups (ADR 0011): never ask. The host's missing Product is already reported
66
- // by the app-context validation.
67
- const {
68
- query: { data: envelope, isPlaceholderData },
69
- loading,
70
- } = usePagedMessageGroups({
71
- search: searchTerm,
72
- page,
73
- perPage: PER_PAGE,
74
- renderType: () => props.renderType,
75
- filters: () => ({
76
- product: appContext.product,
77
- visibility: appContext.isAdmin ? 'all' : 'public',
78
- }),
79
- resetOn: [() => appContext.product],
80
- enabled: () => appContext.product != null,
81
- })
82
-
83
- // Pages loaded so far, indexed by page number: page one replaces everything,
84
- // later pages slot in at their index (a refetch replaces, never duplicates).
85
- const pages = shallowRef<PickerGroup[][]>([])
86
- watch(
87
- envelope,
88
- (value) => {
89
- if (!value) return
90
- const index = value.meta.current_page - 1
91
- pages.value = index === 0 ? [value.data] : [...pages.value.slice(0, index), value.data]
92
- },
93
- { immediate: true },
94
- )
95
- const loadedGroups = computed(() => pages.value.flat())
96
-
97
- // The selected group is pinned on top of the loaded pages when they do not
98
- // contain it: remembered from the last pick, or fetched by id when preselected.
99
- const lastPicked = shallowRef<PickerGroup>()
100
- const missingSelectedId = computed(() => {
101
- const id = modelValue.value
102
- if (!id || loadedGroups.value.some((g) => g.id === id)) return undefined
103
- return lastPicked.value?.id === id ? undefined : id
104
- })
105
- const { data: fetchedSelected } = useMessageGroupQuery(() => missingSelectedId.value ?? '', {
106
- enabled: () => missingSelectedId.value != null,
107
- })
108
- const messageGroups = computed<PickerGroup[]>(() => {
109
- const id = modelValue.value
110
- if (!id || loadedGroups.value.some((g) => g.id === id)) return loadedGroups.value
111
- const pinned =
112
- lastPicked.value?.id === id
113
- ? lastPicked.value
114
- : fetchedSelected.value?.id === id
115
- ? fetchedSelected.value
116
- : undefined
117
- return pinned ? [pinned, ...loadedGroups.value] : loadedGroups.value
118
- })
119
-
120
- const hasMore = computed(
121
- () =>
122
- !isPlaceholderData.value &&
123
- envelope.value != null &&
124
- envelope.value.meta.current_page < envelope.value.meta.last_page,
125
- )
126
- const isLoadingFirstPage = computed(() => page.value === 1 && loading.value)
127
- const isLoadingMore = computed(() => page.value > 1 && loading.value)
39
+ const { data: messageGroups, isLoading } = useMessageGroupsQuery(() => ({
40
+ renderType: props.renderType,
41
+ }))
128
42
 
129
43
  const hideInactive = ref(false)
130
44
 
@@ -133,6 +47,7 @@ function isGroupActive(messages: Array<{ status: MessageStatus }>): boolean {
133
47
  }
134
48
 
135
49
  const items = computed(() => {
50
+ if (!messageGroups.value) return []
136
51
  return messageGroups.value.reduce<
137
52
  Array<{
138
53
  label: string
@@ -165,21 +80,18 @@ const items = computed(() => {
165
80
 
166
81
  function handleUpdate(value: string) {
167
82
  modelValue.value = value
168
- const group = messageGroups.value.find((g) => g.id === value)
169
- lastPicked.value = group
83
+ const group = messageGroups.value?.find((g) => g.id === value)
170
84
  emit('select', { id: value, name: group?.name ?? '' })
171
85
  }
172
86
  </script>
173
87
 
174
88
  <template>
175
89
  <USelectMenu
176
- v-model:search-term="searchTerm"
177
90
  :model-value="modelValue"
178
91
  :items="items"
179
92
  value-key="value"
180
- :loading="isLoadingFirstPage"
93
+ :loading="isLoading"
181
94
  searchable
182
- ignore-filter
183
95
  :placeholder="placeholder"
184
96
  :search-input="{ placeholder: searchPlaceholder }"
185
97
  :data-testid="messageGroupSelectTestIds.root"
@@ -200,18 +112,5 @@ function handleUpdate(value: string) {
200
112
  </span>
201
113
  </div>
202
114
  </template>
203
-
204
- <template v-if="hasMore || isLoadingMore" #content-bottom>
205
- <div class="flex justify-center px-2 py-1.5 border-t border-default">
206
- <UButton
207
- variant="link"
208
- size="xs"
209
- :loading="isLoadingMore"
210
- :label="t('common.messageGroupSelect.load_more')"
211
- :data-testid="messageGroupSelectTestIds.loadMore"
212
- @click="page++"
213
- />
214
- </div>
215
- </template>
216
115
  </USelectMenu>
217
116
  </template>
@@ -1,5 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import {
3
+ useMessageGroupsQuery,
3
4
  useDeleteMessageGroupMutation,
4
5
  usePublishMessageGroupMutation,
5
6
  useUnpublishMessageGroupMutation,
@@ -9,6 +10,7 @@ import type {
9
10
  RenderType,
10
11
  } from '@dev.smartpricing/message-composer-utils/types'
11
12
  import type { MessageActionProperties, MessagesKind } from '@/types/tracking'
13
+ import { computed } from 'vue'
12
14
  import BaseTable from '@/components/TemplateList/BaseTable.vue'
13
15
 
14
16
  const renderType = defineModel<RenderType>('renderType', { required: true })
@@ -24,17 +26,12 @@ const emit = defineEmits<{
24
26
  const { trackEvent } = useTracking()
25
27
  const { t } = useI18n()
26
28
 
27
- // List state + server-side paginated query (ADR 0012)
29
+ // Query
28
30
  const {
29
- state: listState,
30
- setFilters,
31
- setSorting,
32
- setPage,
33
- rows: messageGroups,
34
- meta,
35
- loading: isFetching,
31
+ data: messageGroups,
32
+ isPending: isFetching,
36
33
  refetch: execute,
37
- } = useMessageGroupList(renderType)
34
+ } = useMessageGroupsQuery(computed(() => ({ renderType: renderType.value })))
38
35
 
39
36
  // Polling
40
37
  const interval = setInterval(() => execute(), 15000)
@@ -147,15 +144,8 @@ defineExpose({ execute })
147
144
  <template>
148
145
  <BaseTable
149
146
  :loading="isFetching"
150
- :message-groups="messageGroups"
151
- :meta="meta"
147
+ :message-groups="messageGroups || []"
152
148
  :render-type="renderType"
153
- :filters="listState"
154
- :sorting="listState"
155
- :page="listState.page"
156
- @update:filters="setFilters"
157
- @update:sorting="setSorting"
158
- @update:page="setPage"
159
149
  @edit="navigate"
160
150
  @delete="askDeleteGroup"
161
151
  @duplicate="handleDuplicate"
@@ -10,6 +10,7 @@ import type {
10
10
  } from '@dev.smartpricing/message-composer-utils/types'
11
11
  import {
12
12
  buildLinkHTMLAttributes,
13
+ convertToWhatsappHeaderText,
13
14
  convertToWhatsappMarkdown,
14
15
  getTipTapExtensions,
15
16
  sanitizeContent,
@@ -25,7 +26,7 @@ const { t } = useI18n()
25
26
 
26
27
  const props = withDefaults(
27
28
  defineProps<{
28
- preset: 'whatsapp' | 'email'
29
+ preset: 'whatsapp' | 'whatsapp-header' | 'email'
29
30
  characterLimit?: number
30
31
  dynamicValues?: boolean
31
32
  defaultContext?: string
@@ -277,9 +278,12 @@ const toolbarItems = computed(() => {
277
278
  // WhatsApp markdown-aware character count
278
279
  // Meta counts the final markdown output (with *bold*, _italic_ marks),
279
280
  // so we must display that count instead of TipTap's document-model count.
281
+ const isWhatsapp = computed(() => props.preset === 'whatsapp' || props.preset === 'whatsapp-header')
280
282
  const whatsappCharCount = computed(() => {
281
- if (props.preset !== 'whatsapp' || !props.characterLimit || !text.value) return 0
282
- return convertToWhatsappMarkdown(text.value).length
283
+ if (!isWhatsapp.value || !props.characterLimit || !text.value) return 0
284
+ return props.preset === 'whatsapp-header'
285
+ ? convertToWhatsappHeaderText(text.value).length
286
+ : convertToWhatsappMarkdown(text.value).length
283
287
  })
284
288
 
285
289
  // Handlers
@@ -482,7 +486,7 @@ defineExpose({
482
486
  {{ $t('editor.validation.character_limit') }}
483
487
  </span>
484
488
  <span>
485
- {{ preset === 'whatsapp' ? whatsappCharCount : editor.storage.characterCount.characters() }}
489
+ {{ isWhatsapp ? whatsappCharCount : editor.storage.characterCount.characters() }}
486
490
  / {{ characterLimit }}
487
491
  </span>
488
492
  </p>