@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.
- package/frontend-vue/app/components/OverviewView.vue +1 -1
- package/frontend-vue/app/components/PagesView.vue +15 -2
- package/frontend-vue/app/composables/useMarketingPeriod.ts +1 -2
- package/frontend-vue/app/composables/useMarketingRoutes.ts +8 -5
- package/frontend-vue/app/constants.ts +10 -0
- package/frontend-vue/tests/latest-request.test.ts +35 -0
- package/frontend-vue/tests/routes.test.ts +10 -18
- package/package.json +1 -1
|
@@ -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
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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()).
|
|
11
|
-
expect(campaignsLink()).
|
|
12
|
-
|
|
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('
|
|
22
|
-
expect(
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
|
|
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
|
})
|