@antelopejs/dms-marketing 0.2.3 → 0.2.5

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,6 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, onMounted, watch } from 'vue'
3
3
  import { useI18n } from '#dms/frontend-module'
4
+ import { MS_PER_DAY } from '../constants'
4
5
  import { useChildId } from '../composables/useChildId'
5
6
  import { useLatestRequest } from '../composables/useLatestRequest'
6
7
  import {
@@ -10,7 +11,6 @@ import {
10
11
  import {
11
12
  DEFAULT_PERIOD_PRESET,
12
13
  MARKETING_PERIOD_PRESETS,
13
- MS_PER_DAY,
14
14
  periodDays,
15
15
  useMarketingPeriod,
16
16
  } from '../composables/useMarketingPeriod'
@@ -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')"
@@ -1,4 +1,5 @@
1
1
  import { computed, type ComputedRef } from 'vue'
2
+ import { MS_PER_DAY } from '../constants'
2
3
 
3
4
  /**
4
5
  * Bridge between the DMS period selector and the marketing endpoints.
@@ -9,8 +10,6 @@ import { computed, type ComputedRef } from 'vue'
9
10
  * carry the window as a query parameter and have to map it back onto a preset.
10
11
  */
11
12
 
12
- export const MS_PER_DAY = 86_400_000
13
-
14
13
  export const DEFAULT_PERIOD_DAYS = 7
15
14
  export const DEFAULT_PERIOD = `${DEFAULT_PERIOD_DAYS}d`
16
15
 
@@ -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,10 @@
1
+ /**
2
+ * Module-wide constants, imported explicitly.
3
+ *
4
+ * Deliberately outside `app/composables`, `app/types` and `app/utils`: the DMS
5
+ * frontend builder auto-imports everything those directories export into the
6
+ * shared namespace, where a generic name silently shadows the core's. This
7
+ * file is never scanned, so anything here has to be imported by hand.
8
+ */
9
+
10
+ export const MS_PER_DAY = 86_400_000
@@ -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.3",
3
+ "version": "0.2.5",
4
4
  "license": "Apache-2.0",
5
5
  "repository": {
6
6
  "type": "git",