@antelopejs/dms-marketing 0.2.2 → 0.2.4

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.
@@ -81,6 +81,7 @@ let searchTimer: ReturnType<typeof setTimeout> | null = null
81
81
  const {
82
82
  data: inventory,
83
83
  loading: loadingPages,
84
+ settled: pagesSettled,
84
85
  failed: pagesFailed,
85
86
  run: loadPages,
86
87
  } = useLatestRequest<MarketingPages>(() =>
@@ -100,6 +101,16 @@ const trackerEnabled = computed(() => inventory.value?.trackerEnabled ?? true)
100
101
  // attempt, and must not clear the one that says the site list never loaded.
101
102
  const failed = computed(() => websitesFailed.value || pagesFailed.value)
102
103
 
104
+ // A selected site means an inventory request is expected. Keep the surface
105
+ // visibly loading through the first answer instead of rendering an empty
106
+ // master/detail shell while websites, pages and the preview requests settle.
107
+ const loading = computed(
108
+ () =>
109
+ loadingWebsites.value
110
+ || loadingPages.value
111
+ || (!!selectedWebsiteId.value && !pagesSettled.value),
112
+ )
113
+
103
114
  // Land on something rather than on an empty pane, but never override a path
104
115
  // that was linked to or typed. Watching the answer rather than writing from
105
116
  // the fetch keeps superseded ones out: only the newest ever reaches here.
@@ -334,8 +345,10 @@ async function resetSiteHeatmaps() {
334
345
  />
335
346
 
336
347
  <template v-else>
348
+ <USkeleton v-if="loading" class="h-96 w-full" />
349
+
337
350
  <UAlert
338
- v-if="truncated"
351
+ v-else-if="truncated"
339
352
  icon="i-ph-list"
340
353
  color="neutral"
341
354
  variant="subtle"
@@ -353,7 +366,7 @@ async function resetSiteHeatmaps() {
353
366
  />
354
367
 
355
368
  <DmsMasterDetail
356
- v-else
369
+ v-else-if="!loading"
357
370
  v-model="selectedPath"
358
371
  :items="listItems"
359
372
  :list-label="t('page.marketing.pages.inventory')"
@@ -16,12 +16,15 @@ interface PagesLink {
16
16
  }
17
17
 
18
18
  /** Query without the empty keys — a bare link must stay bare. */
19
- function withQuery(base: string, query: Record<string, string | null | undefined>) {
20
- const entries = Object.entries(query).filter(([, value]) => !!value)
21
- return {
22
- path: base,
23
- query: Object.fromEntries(entries) as Record<string, string>,
19
+ function withQuery(base: string, query: Record<string, string | null | undefined>): string {
20
+ const params = new URLSearchParams()
21
+ for (const [key, value] of Object.entries(query)) {
22
+ if (value) {
23
+ params.set(key, value)
24
+ }
24
25
  }
26
+ const suffix = params.toString()
27
+ return suffix ? `${base}?${suffix}` : base
25
28
  }
26
29
 
27
30
  /** The tracked-pages surface, optionally on a given site, path and window. */
@@ -0,0 +1,35 @@
1
+ import { nextTick } from 'vue'
2
+ import { describe, expect, it } from 'vitest'
3
+ import { useLatestRequest } from '../app/composables/useLatestRequest'
4
+
5
+ describe('useLatestRequest loading states', () => {
6
+ it('is unsettled immediately and settles with the fetched path inventory', async () => {
7
+ let resolve: ((value: { pages: { path: string }[] }) => void) | undefined
8
+ const request = useLatestRequest(() => new Promise((done) => {
9
+ resolve = done
10
+ }))
11
+
12
+ const pending = request.run()
13
+ expect(request.settled.value).toBe(false)
14
+ expect(request.loading.value).toBe(true)
15
+ expect(request.data.value).toBeNull()
16
+
17
+ resolve?.({ pages: [{ path: '/pricing' }] })
18
+ await pending
19
+ await nextTick()
20
+
21
+ expect(request.settled.value).toBe(true)
22
+ expect(request.loading.value).toBe(false)
23
+ expect(request.data.value?.pages[0]?.path).toBe('/pricing')
24
+ })
25
+
26
+ it('settles an empty response without leaving the empty path state loading', async () => {
27
+ const request = useLatestRequest(async () => ({ pages: [] }))
28
+
29
+ await request.run()
30
+
31
+ expect(request.settled.value).toBe(true)
32
+ expect(request.loading.value).toBe(false)
33
+ expect(request.data.value?.pages).toEqual([])
34
+ })
35
+ })
@@ -7,25 +7,17 @@ import {
7
7
 
8
8
  describe('module links', () => {
9
9
  it('stay bare when nothing is selected', () => {
10
- expect(pagesLink()).toEqual({ path: '/modules/marketing/pages', query: {} })
11
- expect(campaignsLink()).toEqual({
12
- path: '/modules/marketing/campaigns',
13
- query: {},
14
- })
15
- expect(funnelsLink()).toEqual({
16
- path: '/modules/marketing/funnels',
17
- query: {},
18
- })
10
+ expect(pagesLink()).toBe('/modules/marketing/pages')
11
+ expect(campaignsLink()).toBe('/modules/marketing/campaigns')
12
+ expect(funnelsLink()).toBe('/modules/marketing/funnels')
19
13
  })
20
14
 
21
- it('carry only the selections that have a value', () => {
22
- expect(pagesLink({ website: 'w1', path: undefined, period: '7d' })).toEqual({
23
- path: '/modules/marketing/pages',
24
- query: { website: 'w1', period: '7d' },
25
- })
26
- expect(funnelsLink({ website: 'w1', funnel: null })).toEqual({
27
- path: '/modules/marketing/funnels',
28
- query: { website: 'w1' },
29
- })
15
+ it('serializes the exact button route and encodes selected values', () => {
16
+ expect(funnelsLink({ website: 'test-site-1' })).toBe(
17
+ '/modules/marketing/funnels?website=test-site-1',
18
+ )
19
+ expect(pagesLink({ website: 'w 1', path: '/pricing?x=1', period: '7d' })).toBe(
20
+ '/modules/marketing/pages?website=w+1&path=%2Fpricing%3Fx%3D1&period=7d',
21
+ )
30
22
  })
31
23
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antelopejs/dms-marketing",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "license": "Apache-2.0",
5
5
  "repository": {
6
6
  "type": "git",