@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.
@@ -1,36 +1,16 @@
1
1
  <script setup lang="ts">
2
2
  import {
3
3
  MESSAGE_GROUP_CATEGORIES,
4
- MESSAGE_GROUP_LIST_VISIBILITY,
5
- MESSAGE_GROUP_PRODUCTS,
6
4
  MESSAGE_STATUS,
7
- type MessageGroupListSort,
8
5
  type MessageGroupWithMessageSummary,
9
- type PaginationMeta,
10
6
  type RenderType,
11
7
  } from '@dev.smartpricing/message-composer-utils/types'
12
8
  import StatusCell from '@/components/TemplateList/StatusCell.vue'
13
9
  import { useI18n } from 'vue-i18n'
14
- import { computed, ref, watch } from 'vue'
10
+ import { ref, watch } from 'vue'
15
11
  import { formatDate } from '@dev.smartpricing/message-composer-utils/utils'
16
12
  import type { TableColumn } from '#ui/types'
17
13
  import type { TrackingEventMap } from '@/types/tracking'
18
- import {
19
- ALL_FILTER,
20
- type MessageGroupListFilters,
21
- type MessageGroupListSorting,
22
- } from '@/composables/useMessageGroupList'
23
-
24
- const props = defineProps<{
25
- messageGroups: MessageGroupWithMessageSummary[]
26
- /** Envelope meta of the current page; undefined until the first page arrives. */
27
- meta?: PaginationMeta
28
- loading: boolean
29
- renderType: RenderType
30
- filters: MessageGroupListFilters
31
- sorting: MessageGroupListSorting
32
- page: number
33
- }>()
34
14
 
35
15
  const emit = defineEmits<{
36
16
  edit: [MessageGroupWithMessageSummary]
@@ -38,9 +18,6 @@ const emit = defineEmits<{
38
18
  duplicate: [MessageGroupWithMessageSummary]
39
19
  publish: [MessageGroupWithMessageSummary]
40
20
  unpublish: [MessageGroupWithMessageSummary]
41
- 'update:filters': [Partial<MessageGroupListFilters>]
42
- 'update:sorting': [MessageGroupListSorting]
43
- 'update:page': [number]
44
21
  }>()
45
22
 
46
23
  const appContext = useAppContextStore()
@@ -109,40 +86,49 @@ function rowActions(row: MessageGroupWithMessageSummary) {
109
86
  ]
110
87
  }
111
88
 
112
- // Column ids of sortable columns are the server's `sort` values (ADR 0012);
113
- // preview and status are aggregates and stay unsortable.
114
89
  const columns: TableColumn<MessageGroupWithMessageSummary>[] = [
115
90
  {
116
- id: 'name' satisfies MessageGroupListSort,
91
+ id: 'name',
117
92
  accessorKey: 'name',
118
93
  header: (ctx) => getSortableHeader(ctx, t('pages.message_library.headers.name')),
119
94
  },
120
95
  {
121
- id: 'category' satisfies MessageGroupListSort,
96
+ id: 'category',
122
97
  accessorKey: 'category',
123
98
  header: (ctx) => getSortableHeader(ctx, t('pages.message_library.headers.category')),
124
- },
125
- {
126
- id: 'product',
127
- accessorKey: 'product',
128
- enableSorting: false,
129
- header: () => t('pages.message_library.headers.product'),
99
+ filterFn: (row, columnId, filterValue) => {
100
+ if (filterValue === 'all') return true
101
+ return row.original.category.toLowerCase().includes(filterValue.toLowerCase())
102
+ },
130
103
  },
131
104
  {
132
105
  id: 'preview',
133
106
  accessorFn: (row) => getLocalizedMessage(row)?.preview,
134
- enableSorting: false,
135
- header: () => t('pages.message_library.headers.preview'),
107
+ header: (ctx) => getSortableHeader(ctx, t('pages.message_library.headers.preview')),
136
108
  },
137
109
  {
138
110
  id: 'status',
139
111
  accessorFn: (row) => row.messages.map((m) => m.status).join(', '),
140
- enableSorting: false,
141
- header: () => t('pages.message_library.headers.status'),
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
+ },
142
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
+ // },
143
128
  {
144
- id: 'updated_at' satisfies MessageGroupListSort,
129
+ id: 'updated_at',
145
130
  accessorFn: (row) => formatDate(row.updated_at),
131
+ // header: () => t('pages.message_library.headers.updated_at'),
146
132
  header: (ctx) => getSortableHeader(ctx, t('pages.message_library.headers.updated_at')),
147
133
  },
148
134
  {
@@ -158,70 +144,77 @@ const columns: TableColumn<MessageGroupWithMessageSummary>[] = [
158
144
  },
159
145
  ]
160
146
 
161
- // The table's sorting model mirrors the server sort; TanStack never sorts locally (manualSorting).
162
- function toTableSorting(sorting: MessageGroupListSorting) {
163
- return [{ id: sorting.sort, desc: sorting.order === 'desc' }]
164
- }
147
+ const sorting = ref([
148
+ {
149
+ id: 'updated_at',
150
+ desc: true,
151
+ },
152
+ ])
165
153
 
166
- const sorting = ref(toTableSorting(props.sorting))
154
+ const props = defineProps<{
155
+ messageGroups: MessageGroupWithMessageSummary[]
156
+ loading: boolean
157
+ renderType: RenderType
158
+ }>()
167
159
 
168
- watch(
169
- () => props.sorting,
170
- (value) => {
171
- sorting.value = toTableSorting(value)
172
- },
173
- )
160
+ const filteredMessageGroups = computed(() => {
161
+ if (appContext.isAdmin) return props.messageGroups
162
+ return props.messageGroups.filter((mg) => mg.visibility !== 'internal')
163
+ })
174
164
 
175
- watch(sorting, (val) => {
176
- const [first] = val
177
- if (!first) return
178
- const next: MessageGroupListSorting = {
179
- sort: first.id as MessageGroupListSort,
180
- order: first.desc ? 'desc' : 'asc',
181
- }
182
- if (next.sort === props.sorting.sort && next.order === props.sorting.order) return
183
- emit('update:sorting', next)
184
- trackEvent('table_sorting', {
185
- messages_kind: messagesKind.value,
186
- sort_column: next.sort,
187
- })
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
+ ]
188
176
  })
189
177
 
190
- // Select items: the "all" entry first, then one translated entry per enum value.
191
- function toSelectItems<T extends string>(
192
- values: readonly T[],
193
- keyPrefix: string,
194
- allLabel: string,
195
- ) {
178
+ const statusItems = computed(() => {
196
179
  return [
197
- { label: allLabel, value: ALL_FILTER },
198
- ...values.map((value) => ({ label: t(`${keyPrefix}.${value}`), value })),
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
+ })),
199
189
  ]
200
- }
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[]>([])
201
199
 
202
- const categoryItems = computed(() =>
203
- toSelectItems(MESSAGE_GROUP_CATEGORIES, 'categories', t('common.all')),
204
- )
205
- const statusItems = computed(() => toSelectItems(MESSAGE_STATUS, 'status', t('common.all')))
206
- const productItems = computed(() =>
207
- toSelectItems(MESSAGE_GROUP_PRODUCTS, 'products', t('pages.message_library.all_products')),
208
- )
209
-
210
- const visibilityItems = computed(() =>
211
- MESSAGE_GROUP_LIST_VISIBILITY.map((visibility) => ({
212
- label: t(`pages.message_library.visibility.${visibility}`),
213
- value: visibility,
214
- })),
215
- )
200
+ const table = useTemplateRef('table')
216
201
 
217
202
  const messagesKind = computed(() => (props.renderType === 'email_v1' ? 'email' : 'whatsapp'))
218
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
+
219
213
  function trackFilters(overrides: Partial<TrackingEventMap['change_filters']> = {}) {
220
214
  trackEvent('change_filters', {
221
- search_term: props.filters.search,
222
- category: props.filters.category,
223
- status: props.filters.status,
224
- product: props.filters.product,
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) || '',
225
218
  messages_kind: messagesKind.value,
226
219
  ...overrides,
227
220
  })
@@ -232,24 +225,19 @@ const trackSearchFilter = useDebounceFn((value: string) => {
232
225
  }, 500)
233
226
 
234
227
  function onSearchInput(value: string) {
235
- emit('update:filters', { search: value })
228
+ table.value?.tableApi?.getColumn('name')?.setFilterValue(value)
236
229
  trackSearchFilter(value)
237
230
  }
238
231
 
239
- // USelect emits the option value as a plain string; the items are built from the typed enums.
240
- function onFilter(key: Exclude<keyof MessageGroupListFilters, 'search'>, value: string) {
241
- emit('update:filters', { [key]: value } as Partial<MessageGroupListFilters>)
242
- if (key !== 'visibility') trackFilters({ [key]: value })
232
+ function onCategoryFilter(value: string) {
233
+ table.value?.tableApi?.getColumn('category')?.setFilterValue(value)
234
+ trackFilters({ category: value })
243
235
  }
244
236
 
245
- const paginationSummary = computed(() => {
246
- if (!props.meta || props.meta.from === null || props.meta.to === null) return ''
247
- return t('pages.message_library.pagination_summary', {
248
- from: props.meta.from,
249
- to: props.meta.to,
250
- total: props.meta.total,
251
- })
252
- })
237
+ function onStatusFilter(value: string) {
238
+ table.value?.tableApi?.getColumn('status')?.setFilterValue(value)
239
+ trackFilters({ status: value })
240
+ }
253
241
  </script>
254
242
 
255
243
  <template>
@@ -259,46 +247,38 @@ const paginationSummary = computed(() => {
259
247
  >
260
248
  <div class="flex gap-2">
261
249
  <UInput
262
- :model-value="filters.search"
250
+ :model-value="table?.tableApi?.getColumn('name')?.getFilterValue() as string"
263
251
  class="max-w-xs"
264
252
  leading-icon="ph:magnifying-glass-bold"
265
253
  :placeholder="t('pages.message_library.search_placeholder')"
266
254
  :data-testid="templateListTableTestIds.searchInput"
267
- @update:model-value="onSearchInput(String($event))"
255
+ @update:model-value="onSearchInput($event)"
268
256
  />
269
257
  <USelect
270
- :model-value="filters.category"
258
+ :model-value="table?.tableApi?.getColumn('category')?.getFilterValue() as string"
271
259
  class="max-w-48"
272
260
  :placeholder="t('pages.message_library.category_placeholder')"
273
261
  :items="categoryItems"
274
262
  :data-testid="templateListTableTestIds.categorySelect"
275
- @update:model-value="onFilter('category', $event)"
263
+ @update:model-value="onCategoryFilter($event)"
276
264
  />
277
265
  <USelect
278
- :model-value="filters.status"
266
+ :model-value="table?.tableApi?.getColumn('status')?.getFilterValue() as string"
279
267
  class="max-w-48"
280
268
  :placeholder="t('pages.message_library.status_placeholder')"
281
269
  :items="statusItems"
282
270
  :data-testid="templateListTableTestIds.statusSelect"
283
- @update:model-value="onFilter('status', $event)"
284
- />
285
- <USelect
286
- :model-value="filters.product"
287
- class="max-w-48"
288
- :placeholder="t('pages.message_library.product_placeholder')"
289
- :items="productItems"
290
- :data-testid="templateListTableTestIds.productSelect"
291
- @update:model-value="onFilter('product', $event)"
292
- />
293
- <USelect
294
- v-if="appContext.isAdmin"
295
- :model-value="filters.visibility"
296
- class="max-w-48"
297
- :placeholder="t('pages.message_library.visibility_placeholder')"
298
- :items="visibilityItems"
299
- :data-testid="templateListTableTestIds.visibilitySelect"
300
- @update:model-value="onFilter('visibility', $event)"
271
+ @update:model-value="onStatusFilter($event)"
301
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
+ /> -->
302
282
  </div>
303
283
 
304
284
  <UCard
@@ -315,10 +295,10 @@ const paginationSummary = computed(() => {
315
295
  tr: 'hover:bg-muted',
316
296
  root: 'overflow-visible static',
317
297
  }"
318
- :data="messageGroups"
298
+ :data="filteredMessageGroups"
319
299
  :columns="columns"
320
- :sorting-options="{ manualSorting: true, enableSortingRemoval: false }"
321
300
  sticky
301
+ ref="table"
322
302
  v-model:sorting="sorting"
323
303
  :data-testid="templateListTableTestIds.table"
324
304
  >
@@ -349,16 +329,6 @@ const paginationSummary = computed(() => {
349
329
  />
350
330
  </template>
351
331
 
352
- <template #product-cell="{ row }">
353
- <UBadge
354
- :label="t(`products.${row.original.product}`)"
355
- size="md"
356
- color="neutral"
357
- variant="outline"
358
- :data-testid="`${templateListTableTestIds.rowProductBadge}-${row.original.id}`"
359
- />
360
- </template>
361
-
362
332
  <template #status-cell="{ row }">
363
333
  <StatusCell :items="row.original.messages" />
364
334
  </template>
@@ -379,24 +349,6 @@ const paginationSummary = computed(() => {
379
349
  />
380
350
  </template>
381
351
  </UTable>
382
-
383
- <template v-if="meta" #footer>
384
- <div class="flex items-center justify-between gap-4">
385
- <span
386
- class="text-sm text-muted"
387
- :data-testid="templateListTableTestIds.paginationSummary"
388
- >
389
- {{ paginationSummary }}
390
- </span>
391
- <UPagination
392
- :page="page"
393
- :total="meta.total"
394
- :items-per-page="meta.per_page"
395
- :data-testid="templateListTableTestIds.pagination"
396
- @update:page="emit('update:page', $event)"
397
- />
398
- </div>
399
- </template>
400
352
  </UCard>
401
353
  </div>
402
354
  </template>
@@ -112,10 +112,7 @@ function isMultimedia(model: HeaderBlockV1): model is WaHeaderTypeV1Url {
112
112
  <div v-if="model.type === 'text'" class="mt-3">
113
113
  <RichTextEditor
114
114
  v-model:text="model.content"
115
- preset="whatsapp"
116
- bold
117
- italic
118
- emoji
115
+ preset="whatsapp-header"
119
116
  improve
120
117
  :character-limit="WA_HEADER_TEXT_MAX_LENGTH"
121
118
  dynamic-values
@@ -9,6 +9,7 @@ import { computed } from 'vue'
9
9
  import {
10
10
  generateWhatsappHtml,
11
11
  getTipTapExtensions,
12
+ sanitizeContent,
12
13
  } from '@dev.smartpricing/message-composer-utils/utils'
13
14
  import { useResolvedParametersQuery } from '~/queries/compilation'
14
15
 
@@ -51,9 +52,10 @@ const bodyHtml = computed(() => {
51
52
  const headerHtml = computed(() => {
52
53
  if (props.message.body.header.type !== 'text') return ''
53
54
  if (!props.message.body.header.content) return ''
55
+ const extensions = getTipTapExtensions({ preset: 'whatsapp-header' })
54
56
  const html = generateHTML(
55
- props.message.body.header.content,
56
- getTipTapExtensions({ preset: 'whatsapp-header' }),
57
+ sanitizeContent(props.message.body.header.content, extensions),
58
+ extensions,
57
59
  ).replaceAll('<p></p>', '<p>&nbsp;</p>')
58
60
  return replaceMentionsWithValues(html)
59
61
  })
@@ -1,7 +1,10 @@
1
1
  <script setup lang="ts">
2
2
  import type { JSONContent } from '@tiptap/vue-3'
3
3
  import { generateHTML } from '@tiptap/vue-3'
4
- import { getTipTapExtensions } from '@dev.smartpricing/message-composer-utils/utils'
4
+ import {
5
+ getTipTapExtensions,
6
+ sanitizeContent,
7
+ } from '@dev.smartpricing/message-composer-utils/utils'
5
8
  import RichTextEditor from '@/components/Editor/RichTextEditor.vue'
6
9
  import { useAppContextStore } from '@/stores/appContext'
7
10
  import { WA_HEADER_TEXT_MAX_LENGTH } from '@dev.smartpricing/message-composer-utils/types'
@@ -23,7 +26,8 @@ const appContext = useAppContextStore()
23
26
  // Generate HTML for preview of base language with rich text support
24
27
  function generatePreviewHtml(content: JSONContent | undefined): string {
25
28
  if (!content) return ''
26
- return generateHTML(content, getTipTapExtensions({ preset: 'whatsapp-header' })).replaceAll(
29
+ const extensions = getTipTapExtensions({ preset: 'whatsapp-header' })
30
+ return generateHTML(sanitizeContent(content, extensions), extensions).replaceAll(
27
31
  '<p></p>',
28
32
  '<p>&nbsp;</p>',
29
33
  )
@@ -45,10 +49,7 @@ function generatePreviewHtml(content: JSONContent | undefined): string {
45
49
  <div class="flex-1">
46
50
  <RichTextEditor
47
51
  v-model:text="targetContent"
48
- preset="whatsapp"
49
- bold
50
- italic
51
- emoji
52
+ preset="whatsapp-header"
52
53
  dynamic-values
53
54
  :character-limit="WA_HEADER_TEXT_MAX_LENGTH"
54
55
  :data-testid="whatsappTranslatorHeaderTestIds.targetEditor"
@@ -12,8 +12,7 @@ export interface ComposerContext {
12
12
  /** @deprecated No longer used — auth is cookie-based via platform layer */
13
13
  authenticationToken?: Ref<string>
14
14
  organization_id?: Ref<string>
15
- /** The Product the host embeds the composer in (ADR 0011). Required. */
16
- product: Ref<AppContextConfig['product']>
15
+ product?: Ref<AppContextConfig['product']>
17
16
  strategy?: Ref<AppContextConfig['strategy']>
18
17
  channels?: Ref<AppContextConfig['channels']>
19
18
  languages?: Ref<string[]>
@@ -34,8 +33,7 @@ export function defineComposerContext(context: ComposerContext): void {
34
33
  useNuxtApp().vueApp.provide(COMPOSER_CONTEXT_KEY, context)
35
34
  }
36
35
 
37
- // Server render never reads the context; the store reads every field defensively.
38
- const SERVER_FALLBACK = {} as ComposerContext
36
+ const SERVER_FALLBACK: ComposerContext = {}
39
37
 
40
38
  export function useComposerContext(): ComposerContext {
41
39
  if (import.meta.server) return SERVER_FALLBACK
@@ -13,7 +13,6 @@ import type {
13
13
  MessageStatus,
14
14
  MessageGroup,
15
15
  } from '@dev.smartpricing/message-composer-utils/types'
16
- import { useAppContextStore } from '@/stores/appContext'
17
16
 
18
17
  interface TemplateData<T = Message | PostEntity<Message>> {
19
18
  messageGroup: { name: string; category: MessageGroupCategory; brand_id?: string | null }
@@ -28,7 +27,6 @@ interface UseTemplateOperationsOptions {
28
27
  export function useTemplateOperations(options: UseTemplateOperationsOptions = {}) {
29
28
  const saving = ref(false)
30
29
  const saved = ref(false)
31
- const appContext = useAppContextStore()
32
30
 
33
31
  // Hoist composables to setup scope (they use inject() internally)
34
32
  const { mutateAsync: createMessageGroup } = useCreateMessageGroupMutation()
@@ -41,17 +39,9 @@ export function useTemplateOperations(options: UseTemplateOperationsOptions = {}
41
39
  async function createTemplate(data: TemplateData, status: MessageStatus = 'draft') {
42
40
  saving.value = true
43
41
  try {
44
- // Every Message Group belongs to the context Product (ADR 0011).
45
- const product = appContext.product
46
- if (!product) {
47
- throw new Error(
48
- '[message-composer] Cannot create a template: no Product in the composer context',
49
- )
50
- }
51
- const createdMessageGroup = await createMessageGroup({
52
- ...data.messageGroup,
53
- product,
54
- } as PostEntity<MessageGroup>)
42
+ const createdMessageGroup = await createMessageGroup(
43
+ data.messageGroup as PostEntity<MessageGroup>,
44
+ )
55
45
 
56
46
  if (createdMessageGroup?.id) {
57
47
  for (const message of Object.values(data.messages)) {
@@ -1,6 +1,5 @@
1
1
  <template>
2
2
  <SNavigationPage :panel-props="{ ui: { body: 'p-0 sm:p-0 gap-0 sm:gap-0 overflow-hidden' } }">
3
- <CommonAppContextErrors />
4
3
  <slot />
5
4
  </SNavigationPage>
6
5
  </template>
@@ -1,5 +1,4 @@
1
1
  import { useQuery } from '@pinia/colada'
2
- import type { DynamicValuesResponse } from '@dev.smartpricing/message-composer-utils/types'
3
2
  import { dynamicValuesApi } from '~/api'
4
3
  import { useAppContextStore } from '~/stores/appContext'
5
4
 
@@ -7,13 +6,8 @@ export function useDynamicValuesQuery() {
7
6
  const { product, mainLanguage } = storeToRefs(useAppContextStore())
8
7
 
9
8
  return useQuery({
10
- key: () => ['dynamic-values', product.value ?? '', mainLanguage.value],
11
- // No Product, no request: the picker stays empty and the store lists the
12
- // missing Product in `validationErrors`.
13
- query: () => {
14
- if (!product.value) return Promise.resolve<DynamicValuesResponse>({})
15
- return dynamicValuesApi.get(product.value, mainLanguage.value || 'en')
16
- },
9
+ key: () => ['dynamic-values', product.value, mainLanguage.value],
10
+ query: () => dynamicValuesApi.get(product.value || 'browser', mainLanguage.value || 'en'),
17
11
  staleTime: 30 * 60 * 1000,
18
12
  })
19
13
  }
@@ -4,38 +4,36 @@ import type {
4
4
  MessageGroup,
5
5
  MessageGroupWithMessageSummary,
6
6
  PostEntity,
7
+ RenderType,
7
8
  } from '@dev.smartpricing/message-composer-utils/types'
8
9
  import { messageGroupsApi } from '~/api'
9
- import type { ListMessageGroupsOptions } from '~/api/messageGroups'
10
10
 
11
11
  // QUERIES
12
- /**
13
- * One page of the server-side filtered list, in the Laravel envelope (ADR 0012).
14
- * The previous page stays as placeholder data while the next one loads, so
15
- * paging and filtering never flash an empty table.
16
- */
17
- export function useMessageGroupListQuery(
18
- options: MaybeRefOrGetter<ListMessageGroupsOptions>,
19
- queryOptions: { enabled?: MaybeRefOrGetter<boolean> } = {},
12
+ export function useMessageGroupsQuery(
13
+ options?: MaybeRefOrGetter<{
14
+ renderType?: RenderType
15
+ tags?: string[]
16
+ tagsMode?: 'and' | 'or'
17
+ }>,
18
+ _queryOptions?: { polling?: boolean },
20
19
  ) {
21
- return useQuery({
20
+ const query = useQuery({
22
21
  key: () => ['message-groups', JSON.stringify(toValue(options))],
23
- query: () => messageGroupsApi.list(toValue(options)),
22
+ query: () => messageGroupsApi.getAll(toValue(options)),
24
23
  staleTime: 0,
25
- placeholderData: (previousData) => previousData,
26
- enabled: queryOptions.enabled,
27
24
  })
25
+
26
+ // TODO: Implement polling when Pinia Colada supports it
27
+ // For now, polling can be done manually in the component using setInterval + refetch
28
+
29
+ return query
28
30
  }
29
31
 
30
- export function useMessageGroupQuery(
31
- id: MaybeRefOrGetter<string>,
32
- queryOptions: { enabled?: MaybeRefOrGetter<boolean> } = {},
33
- ) {
32
+ export function useMessageGroupQuery(id: MaybeRefOrGetter<string>) {
34
33
  return useQuery({
35
34
  key: () => ['message-groups', toValue(id)],
36
35
  query: () => messageGroupsApi.getById(toValue(id)),
37
36
  staleTime: 2 * 60 * 1000,
38
- enabled: queryOptions.enabled,
39
37
  })
40
38
  }
41
39
 
@@ -1,8 +1,7 @@
1
1
  import { ref, computed, watch } from 'vue'
2
- import {
3
- MESSAGE_GROUP_PRODUCTS,
4
- type AppContextConfig,
5
- type AppContextState,
2
+ import type {
3
+ AppContextConfig,
4
+ AppContextState,
6
5
  } from '@dev.smartpricing/message-composer-utils/types'
7
6
 
8
7
  export const useAppContextStore = defineStore('appContext', () => {
@@ -12,10 +11,9 @@ export const useAppContextStore = defineStore('appContext', () => {
12
11
 
13
12
  // Proxy from injection with defaults (same interface as before)
14
13
  const organization_id = computed(() => ctx.organization_id?.value ?? '')
15
- // The Product is required by the context type (ADR 0011), but a host can
16
- // still omit it at runtime: read defensively and list it in
17
- // `validationErrors` rather than crashing or falling back to a fake Product.
18
- const product = computed<AppContextConfig['product'] | undefined>(() => ctx.product?.value)
14
+ const product = computed<NonNullable<AppContextConfig['product']>>(
15
+ () => ctx.product?.value ?? 'browser',
16
+ )
19
17
  const strategy = computed<NonNullable<AppContextConfig['strategy']>>(
20
18
  () => ctx.strategy?.value ?? 'native',
21
19
  )
@@ -61,10 +59,6 @@ export const useAppContextStore = defineStore('appContext', () => {
61
59
  const validationErrors = computed<Record<string, string>>(() => {
62
60
  const errors: Record<string, string> = {}
63
61
 
64
- if (!product.value) {
65
- errors.product = `Product is required: pass \`product\` (${MESSAGE_GROUP_PRODUCTS.join(' | ')}) in the composer context`
66
- }
67
-
68
62
  if (channels.value?.includes('whatsapp') && !metaWabaId.value) {
69
63
  errors.metaConfig = 'Meta WABA ID is required for WhatsApp'
70
64
  }
@@ -1,5 +1,3 @@
1
- import type { MessageGroupProduct } from '@dev.smartpricing/message-composer-utils/types'
2
-
3
1
  export type MessagesKind = 'email' | 'whatsapp'
4
2
 
5
3
  export interface MessageActionProperties {
@@ -85,8 +83,6 @@ export interface TrackingEventMap {
85
83
  search_term: string
86
84
  category: string
87
85
  status: string
88
- /** The Product filter; `all` when not narrowed. */
89
- product: MessageGroupProduct | 'all'
90
86
  messages_kind: MessagesKind
91
87
  }
92
88
  change_tab: { messages_kind: MessagesKind }