@antelopejs/dms-marketing 0.6.1 → 0.7.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/README.md +2 -2
- package/dist/config.d.ts +3 -0
- package/dist/config.js +11 -0
- package/dist/config.js.map +1 -1
- package/dist/data-api/websites.d.ts +10 -4
- package/dist/data-api/websites.js +79 -3
- package/dist/data-api/websites.js.map +1 -1
- package/dist/db/models/funnels.model.d.ts +0 -1
- package/dist/db/models/funnels.model.js +0 -18
- package/dist/db/models/funnels.model.js.map +1 -1
- package/dist/db/models/page_snapshots.model.d.ts +2 -1
- package/dist/db/models/page_snapshots.model.js +6 -14
- package/dist/db/models/page_snapshots.model.js.map +1 -1
- package/dist/db/models/websites.model.d.ts +1 -1
- package/dist/db/models/websites.model.js +9 -29
- package/dist/db/models/websites.model.js.map +1 -1
- package/dist/db/tables/funnels.table.d.ts +0 -1
- package/dist/db/tables/funnels.table.js +0 -4
- package/dist/db/tables/funnels.table.js.map +1 -1
- package/dist/db/tables/page_snapshots.table.d.ts +1 -1
- package/dist/db/tables/page_snapshots.table.js +4 -4
- package/dist/db/tables/page_snapshots.table.js.map +1 -1
- package/dist/db/tables/websites.table.d.ts +2 -1
- package/dist/db/tables/websites.table.js +4 -4
- package/dist/db/tables/websites.table.js.map +1 -1
- package/dist/frontend-module.d.ts +6 -0
- package/dist/frontend-module.js +46 -0
- package/dist/frontend-module.js.map +1 -0
- package/dist/implementations/dms-marketing/index.js +0 -5
- package/dist/implementations/dms-marketing/index.js.map +1 -1
- package/dist/index.js +3 -13
- package/dist/index.js.map +1 -1
- package/dist/pages/acquisition/page.d.ts +1 -0
- package/dist/pages/acquisition/page.js +9 -4
- package/dist/pages/acquisition/page.js.map +1 -1
- package/dist/pages/blocks.d.ts +11 -2
- package/dist/pages/blocks.js +43 -5
- package/dist/pages/blocks.js.map +1 -1
- package/dist/pages/funnel/page.d.ts +1 -0
- package/dist/pages/funnel/page.js +1 -0
- package/dist/pages/funnel/page.js.map +1 -1
- package/dist/pages/funnels/page.d.ts +1 -0
- package/dist/pages/funnels/page.js +4 -2
- package/dist/pages/funnels/page.js.map +1 -1
- package/dist/pages/overview/page.d.ts +1 -0
- package/dist/pages/overview/page.js +5 -6
- package/dist/pages/overview/page.js.map +1 -1
- package/dist/pages/pages/page.d.ts +1 -0
- package/dist/pages/pages/page.js +1 -0
- package/dist/pages/pages/page.js.map +1 -1
- package/dist/pages/settings/form.js +1 -0
- package/dist/pages/settings/form.js.map +1 -1
- package/dist/pages/websites/page.d.ts +2 -3
- package/dist/pages/websites/page.js +63 -5
- package/dist/pages/websites/page.js.map +1 -1
- package/dist/routes/dashboard-tracking.js +5 -1
- package/dist/routes/dashboard-tracking.js.map +1 -1
- package/dist/routes/funnels.d.ts +2 -0
- package/dist/routes/funnels.js +6 -5
- package/dist/routes/funnels.js.map +1 -1
- package/dist/routes/public-endpoints.js +1 -1
- package/dist/routes/public-endpoints.js.map +1 -1
- package/dist/routes/snapshot.d.ts +1 -1
- package/dist/routes/snapshot.js +30 -13
- package/dist/routes/snapshot.js.map +1 -1
- package/dist/routes/stats.d.ts +10 -4
- package/dist/routes/stats.js +77 -23
- package/dist/routes/stats.js.map +1 -1
- package/dist/routes/websites.d.ts +5 -1
- package/dist/routes/websites.js +31 -13
- package/dist/routes/websites.js.map +1 -1
- package/dist/services/acquisition-report.d.ts +1 -0
- package/dist/services/acquisition-report.js +1 -0
- package/dist/services/acquisition-report.js.map +1 -1
- package/dist/services/collect.js +7 -2
- package/dist/services/collect.js.map +1 -1
- package/dist/services/dashboard-tracking.d.ts +7 -14
- package/dist/services/dashboard-tracking.js +42 -26
- package/dist/services/dashboard-tracking.js.map +1 -1
- package/dist/services/declarations.d.ts +0 -2
- package/dist/services/declarations.js +0 -11
- package/dist/services/declarations.js.map +1 -1
- package/dist/services/declared-funnel.js +11 -99
- package/dist/services/declared-funnel.js.map +1 -1
- package/dist/services/declared-snapshot.js +8 -37
- package/dist/services/declared-snapshot.js.map +1 -1
- package/dist/services/declared-website.js +5 -46
- package/dist/services/declared-website.js.map +1 -1
- package/dist/services/funnels-table.d.ts +3 -2
- package/dist/services/funnels-table.js +9 -9
- package/dist/services/funnels-table.js.map +1 -1
- package/dist/services/heatmap.d.ts +6 -0
- package/dist/services/heatmap.js +82 -17
- package/dist/services/heatmap.js.map +1 -1
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.js +1 -0
- package/dist/services/index.js.map +1 -1
- package/dist/services/prune.js +6 -1
- package/dist/services/prune.js.map +1 -1
- package/dist/services/realtime.d.ts +3 -0
- package/dist/services/realtime.js +49 -0
- package/dist/services/realtime.js.map +1 -0
- package/dist/services/request-body.d.ts +1 -0
- package/dist/services/request-body.js +18 -0
- package/dist/services/request-body.js.map +1 -1
- package/dist/services/sessionize.d.ts +1 -0
- package/dist/services/sessionize.js +9 -0
- package/dist/services/sessionize.js.map +1 -1
- package/dist/services/snapshots.d.ts +29 -1
- package/dist/services/snapshots.js +151 -26
- package/dist/services/snapshots.js.map +1 -1
- package/dist/services/tenant-website.d.ts +11 -0
- package/dist/services/tenant-website.js +27 -5
- package/dist/services/tenant-website.js.map +1 -1
- package/dist/services/website-report.d.ts +5 -0
- package/dist/services/website-report.js +13 -1
- package/dist/services/website-report.js.map +1 -1
- package/dist/services/workspace-scope.d.ts +31 -0
- package/dist/services/workspace-scope.js +72 -0
- package/dist/services/workspace-scope.js.map +1 -0
- package/dist/types/constants.d.ts +7 -0
- package/dist/types/constants.js +8 -1
- package/dist/types/constants.js.map +1 -1
- package/dist/types/events.d.ts +25 -25
- package/dist/types/snapshots.d.ts +29 -29
- package/dist/types/snapshots.js +1 -1
- package/dist/types/snapshots.js.map +1 -1
- package/dist/utils/funnel-steps-type.d.ts +13 -13
- package/frontend-vue/app/components/ChannelsCard.vue +19 -2
- package/frontend-vue/app/components/Context.vue +50 -376
- package/frontend-vue/app/components/FunnelBuilder.vue +28 -22
- package/frontend-vue/app/components/FunnelFigure.vue +15 -2
- package/frontend-vue/app/components/FunnelReport.vue +3 -0
- package/frontend-vue/app/components/FunnelTemplates.vue +15 -13
- package/frontend-vue/app/components/InstallGuide.vue +1 -1
- package/frontend-vue/app/components/PagePreview.vue +330 -74
- package/frontend-vue/app/components/PagesExplorer.vue +230 -85
- package/frontend-vue/app/components/TopListTabs.vue +51 -18
- package/frontend-vue/app/components/WebsiteCard.vue +394 -0
- package/frontend-vue/app/components/WebsiteCreate.vue +159 -0
- package/frontend-vue/app/components/WebsitePicker.vue +210 -0
- package/frontend-vue/app/components/WebsiteSettings.vue +539 -0
- package/frontend-vue/app/components/WorkspaceScope.vue +119 -0
- package/frontend-vue/app/composables/useContextWebsite.ts +105 -0
- package/frontend-vue/app/composables/useCopyDraft.ts +47 -0
- package/frontend-vue/app/composables/useMarketingApi.ts +92 -4
- package/frontend-vue/app/composables/useMarketingReads.ts +32 -23
- package/frontend-vue/app/composables/useMarketingRoutes.ts +25 -2
- package/frontend-vue/app/composables/useRecordCrumb.ts +17 -0
- package/frontend-vue/app/composables/useSnapshotFrame.ts +35 -1
- package/frontend-vue/app/utils/anchor-drift.ts +58 -0
- package/frontend-vue/app/utils/drift-dismissals.ts +20 -0
- package/frontend-vue/app/utils/fill-height.ts +141 -0
- package/frontend-vue/app/utils/format.ts +21 -2
- package/frontend-vue/app/utils/funnel-draft.ts +22 -0
- package/frontend-vue/app/utils/heatmap.ts +17 -0
- package/frontend-vue/app/utils/page-snapshot.ts +75 -0
- package/frontend-vue/app/utils/snapshot-options.ts +148 -0
- package/frontend-vue/app/utils/snapshotScrollRoot.ts +93 -0
- package/frontend-vue/app/utils/website-card.ts +93 -0
- package/frontend-vue/app/utils/website-purge.ts +95 -0
- package/frontend-vue/app/utils/website-settings.ts +121 -0
- package/frontend-vue/app/utils/website-state.ts +93 -0
- package/frontend-vue/app/utils/workspace-scope.ts +51 -0
- package/frontend-vue/i18n/locales/marketing-en-GB.json +112 -36
- package/frontend-vue/i18n/locales/marketing-fr-FR.json +112 -36
- package/frontend-vue/package.json +1 -1
- package/frontend-vue/tests/anchor-drift.test.ts +59 -0
- package/frontend-vue/tests/build-flags.test.ts +38 -0
- package/frontend-vue/tests/context-bar.test.ts +226 -0
- package/frontend-vue/tests/copy-draft.test.ts +64 -0
- package/frontend-vue/tests/drift-dismissals.test.ts +12 -0
- package/frontend-vue/tests/fill-height.test.ts +110 -0
- package/frontend-vue/tests/format.test.ts +14 -0
- package/frontend-vue/tests/funnel-draft.test.ts +37 -0
- package/frontend-vue/tests/heatmap.test.ts +25 -0
- package/frontend-vue/tests/install-guide.test.ts +25 -0
- package/frontend-vue/tests/marketing-reads.test.ts +20 -6
- package/frontend-vue/tests/page-snapshot.test.ts +62 -0
- package/frontend-vue/tests/record-crumb.test.ts +43 -0
- package/frontend-vue/tests/registration.test.ts +4 -1
- package/frontend-vue/tests/routes.test.ts +21 -0
- package/frontend-vue/tests/save-funnel.test.ts +47 -0
- package/frontend-vue/tests/server-parity.test.ts +3 -0
- package/frontend-vue/tests/snapshot-options.test.ts +148 -0
- package/frontend-vue/tests/snapshot-scroll-root.test.ts +84 -0
- package/frontend-vue/tests/stubs/frontend-module.ts +5 -1
- package/frontend-vue/tests/top-list-tabs.test.ts +158 -0
- package/frontend-vue/tests/website-card.test.ts +119 -0
- package/frontend-vue/tests/website-purge.test.ts +70 -0
- package/frontend-vue/tests/website-settings.test.ts +133 -0
- package/frontend-vue/tests/website-state.test.ts +96 -0
- package/frontend-vue/tests/workspace-scope-control.test.ts +113 -0
- package/frontend-vue/tests/workspace-scope.test.ts +55 -0
- package/package.json +8 -7
- package/skills/dms-marketing/SKILL.md +54 -12
- package/static/snapshot.js +539 -48
- package/static/tracker.js +154 -5
- package/dist/services/readiness.d.ts +0 -2
- package/dist/services/readiness.js +0 -15
- package/dist/services/readiness.js.map +0 -1
- package/frontend-vue/app/components/WebsitesGrid.vue +0 -812
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
|
3
|
-
import { useDmsRoute, useDmsRouter, useI18n } from '#dms/frontend-module'
|
|
4
2
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
3
|
+
computed,
|
|
4
|
+
nextTick,
|
|
5
|
+
onBeforeUnmount,
|
|
6
|
+
onMounted,
|
|
7
|
+
ref,
|
|
8
|
+
watch,
|
|
9
|
+
} from 'vue'
|
|
10
|
+
import { useDmsRoute, useDmsRouter, useI18n } from '#dms/frontend-module'
|
|
11
|
+
// Imported rather than resolved from the registry: a registered component is
|
|
12
|
+
// a lazy chunk of its own, fetched only once a page is selected, so the
|
|
13
|
+
// preview would land a beat after the list, its column empty meanwhile.
|
|
14
|
+
import DmsMarketingPagePreview from './PagePreview.vue'
|
|
15
|
+
import type {
|
|
16
|
+
MarketingPageEntry,
|
|
17
|
+
MarketingPages,
|
|
18
|
+
MarketingPagesSort,
|
|
19
|
+
MarketingWebsite,
|
|
10
20
|
} from '../composables/useMarketingApi'
|
|
11
21
|
import {
|
|
12
|
-
|
|
22
|
+
useDashboardWorkspaces,
|
|
13
23
|
usePageInventory,
|
|
14
24
|
useWebsiteFunnels,
|
|
15
25
|
useWebsites,
|
|
@@ -19,11 +29,16 @@ import { installLink } from '../composables/useMarketingRoutes'
|
|
|
19
29
|
import { PERIOD_SCOPE } from '../constants'
|
|
20
30
|
import { parseFunnelStepsValue } from '../composables/useFunnelSteps'
|
|
21
31
|
import { formatNumber } from '../utils/format'
|
|
32
|
+
import { pageSnapshotBadge } from '../utils/page-snapshot'
|
|
33
|
+
import { measureFillHeight } from '../utils/fill-height'
|
|
34
|
+
import { useMarketingNow } from '../composables/useMarketingNow'
|
|
35
|
+
import { normalizePagePath, selectedPageRow } from '../utils/heatmap'
|
|
22
36
|
import {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
37
|
+
effectiveWorkspace,
|
|
38
|
+
rememberedWorkspace,
|
|
39
|
+
rememberWorkspace,
|
|
40
|
+
WORKSPACE_QUERY,
|
|
41
|
+
} from '../utils/workspace-scope'
|
|
27
42
|
|
|
28
43
|
/**
|
|
29
44
|
* Pages & heatmaps: the paths visitors loaded on the selected website, with
|
|
@@ -44,6 +59,8 @@ const props = withDefaults(
|
|
|
44
59
|
)
|
|
45
60
|
|
|
46
61
|
const SEARCH_DEBOUNCE_MS = 300
|
|
62
|
+
/** The snapshot ages of the rows move on by the minute. */
|
|
63
|
+
const NOW_TICK_MS = 60_000
|
|
47
64
|
const SKELETON_ROWS = 8
|
|
48
65
|
/** Where the list and the preview sit side by side (Tailwind `xl`). */
|
|
49
66
|
const SIDE_BY_SIDE_QUERY = '(min-width: 1280px)'
|
|
@@ -51,10 +68,7 @@ const SIDE_BY_SIDE_QUERY = '(min-width: 1280px)'
|
|
|
51
68
|
const { t, locale } = useI18n()
|
|
52
69
|
const route = useDmsRoute()
|
|
53
70
|
const router = useDmsRouter()
|
|
54
|
-
const api = useMarketingApi()
|
|
55
71
|
const context = useMarketingContext()
|
|
56
|
-
const toast = useToast()
|
|
57
|
-
const { confirm } = useConfirm()
|
|
58
72
|
|
|
59
73
|
const websiteId = computed(() => context.selected.value?.id ?? '')
|
|
60
74
|
const period = usePeriodScope(props.periodScope)
|
|
@@ -85,25 +99,101 @@ const website = computed(
|
|
|
85
99
|
null,
|
|
86
100
|
)
|
|
87
101
|
|
|
88
|
-
/**
|
|
102
|
+
/**
|
|
103
|
+
* A site as its update answered it, in place: the sites are not read again.
|
|
104
|
+
* The pages are, when its snapshot options changed — saving them deleted the
|
|
105
|
+
* site's snapshots, which the rows show.
|
|
106
|
+
*/
|
|
89
107
|
function putWebsite(saved: MarketingWebsite): void {
|
|
90
108
|
const sites = websitesRead.data.value
|
|
109
|
+
const previous = sites?.find((site) => site._id === saved._id)
|
|
91
110
|
if (sites) {
|
|
92
111
|
websitesRead.data.value = sites.map((site) =>
|
|
93
112
|
site._id === saved._id ? saved : site,
|
|
94
113
|
)
|
|
95
114
|
}
|
|
115
|
+
if (
|
|
116
|
+
previous &&
|
|
117
|
+
(previous.snapshotsEnabled !== saved.snapshotsEnabled ||
|
|
118
|
+
previous.snapshotMaskText !== saved.snapshotMaskText)
|
|
119
|
+
) {
|
|
120
|
+
void loadPages()
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// --- Workspace scope ----------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
// A platform owner on the dashboard's own website reads it for another
|
|
127
|
+
// workspace, or all of them (see utils/workspace-scope). Everyone else gets
|
|
128
|
+
// an empty list from the backend, which also refuses the scope itself.
|
|
129
|
+
const workspacesRead = useDashboardWorkspaces(websiteId)
|
|
130
|
+
const workspaceOptions = computed(() =>
|
|
131
|
+
workspacesRead.error.value
|
|
132
|
+
? []
|
|
133
|
+
: (workspacesRead.data.value?.workspaces ?? []),
|
|
134
|
+
)
|
|
135
|
+
const queryWorkspace = () => {
|
|
136
|
+
const value = route.query[WORKSPACE_QUERY]
|
|
137
|
+
return typeof value === 'string' ? value : ''
|
|
138
|
+
}
|
|
139
|
+
/** What the link asks for, else this browser's last pick (once mounted:
|
|
140
|
+
* the storage is the browser's). */
|
|
141
|
+
const wantedWorkspace = ref(queryWorkspace())
|
|
142
|
+
/** The scope the reads use: the own workspace until the selector offers
|
|
143
|
+
* the wanted one. */
|
|
144
|
+
const workspace = computed(() =>
|
|
145
|
+
effectiveWorkspace(wantedWorkspace.value, workspaceOptions.value),
|
|
146
|
+
)
|
|
147
|
+
/** Another workspace's data: read-only, backdrops masked by the backend. */
|
|
148
|
+
const foreign = computed(() => workspace.value !== '')
|
|
149
|
+
/** A wanted scope waits for the selector's answer, so a link to one never
|
|
150
|
+
* reads the own workspace first, nor fails for a viewer it is not for. */
|
|
151
|
+
const scopeReady = computed(
|
|
152
|
+
() =>
|
|
153
|
+
!wantedWorkspace.value ||
|
|
154
|
+
workspacesRead.status.value === 'success' ||
|
|
155
|
+
workspacesRead.status.value === 'error',
|
|
156
|
+
)
|
|
157
|
+
const scopedWebsiteId = computed(() =>
|
|
158
|
+
scopeReady.value ? websiteId.value : '',
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
function selectWorkspace(value: string): void {
|
|
162
|
+
wantedWorkspace.value = value
|
|
163
|
+
rememberWorkspace(value)
|
|
164
|
+
void router.replace({
|
|
165
|
+
query: { ...route.query, [WORKSPACE_QUERY]: value || undefined },
|
|
166
|
+
})
|
|
96
167
|
}
|
|
97
168
|
|
|
169
|
+
// Back and Forward move the scope with the URL.
|
|
170
|
+
watch(queryWorkspace, (value) => {
|
|
171
|
+
wantedWorkspace.value = value || rememberedWorkspace()
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
// A remembered scope lands in the URL too: a copied link carries it.
|
|
175
|
+
watch(workspace, (value) => {
|
|
176
|
+
if (value && queryWorkspace() !== value) {
|
|
177
|
+
void router.replace({ query: { ...route.query, [WORKSPACE_QUERY]: value } })
|
|
178
|
+
}
|
|
179
|
+
})
|
|
180
|
+
|
|
98
181
|
const inventoryRead = usePageInventory({
|
|
99
|
-
websiteId,
|
|
182
|
+
websiteId: scopedWebsiteId,
|
|
100
183
|
period: pagesPeriod,
|
|
101
184
|
search: searchedTerm,
|
|
102
185
|
sort,
|
|
186
|
+
workspace,
|
|
103
187
|
})
|
|
104
188
|
const inventory = computed(() =>
|
|
105
189
|
inventoryRead.error.value ? null : inventoryRead.data.value,
|
|
106
190
|
)
|
|
191
|
+
|
|
192
|
+
// The pages panel holds the workspace selector from the first paint: a
|
|
193
|
+
// server render, or a navigation, reads the workspaces before it draws, so
|
|
194
|
+
// the panel never grows under the reader (the inventory reads alongside).
|
|
195
|
+
await workspacesRead
|
|
196
|
+
|
|
107
197
|
const loading = computed(() => inventoryRead.status.value === 'pending')
|
|
108
198
|
/** A list is on screen (the previous one while another loads), or failed. */
|
|
109
199
|
const settled = computed(
|
|
@@ -118,7 +208,6 @@ const failed = computed(
|
|
|
118
208
|
const loadPages = (): Promise<void> => inventoryRead.refresh()
|
|
119
209
|
|
|
120
210
|
const funnelsRead = useWebsiteFunnels(websiteId)
|
|
121
|
-
const countSiteHeatmap = useHeatmapCountRead(websiteId)
|
|
122
211
|
const funnels = computed(() =>
|
|
123
212
|
funnelsRead.error.value ? [] : (funnelsRead.data.value ?? []),
|
|
124
213
|
)
|
|
@@ -155,6 +244,16 @@ const sampleRateLabel = computed(() => {
|
|
|
155
244
|
const trackerEnabled = computed(() => inventory.value?.trackerEnabled ?? true)
|
|
156
245
|
const waiting = computed(() => context.selected.value?.state === 'waiting')
|
|
157
246
|
|
|
247
|
+
const now = useMarketingNow(NOW_TICK_MS)
|
|
248
|
+
|
|
249
|
+
/** The listed pages, each with what its row says of its snapshot. */
|
|
250
|
+
const rows = computed(() =>
|
|
251
|
+
pages.value.map((page) => ({
|
|
252
|
+
...page,
|
|
253
|
+
badge: pageSnapshotBadge(page.snapshot, now.value, t, locale.value),
|
|
254
|
+
})),
|
|
255
|
+
)
|
|
256
|
+
|
|
158
257
|
function metricOf(page: { pageviews: number; clicks: number }): number {
|
|
159
258
|
return sort.value === 'clicks' ? page.clicks : page.pageviews
|
|
160
259
|
}
|
|
@@ -191,6 +290,55 @@ watch(inventory, selectFirst)
|
|
|
191
290
|
|
|
192
291
|
const previewWrapper = ref<HTMLElement | null>(null)
|
|
193
292
|
|
|
293
|
+
/**
|
|
294
|
+
* Side by side, the list and the preview fill the window below the context
|
|
295
|
+
* bar, each scrolling inside itself: the page does not scroll, and the
|
|
296
|
+
* preview's toolbar and legend stay in sight however long the page or the
|
|
297
|
+
* list. Stacked (below xl), nothing is pinned: null.
|
|
298
|
+
*/
|
|
299
|
+
const panes = ref<HTMLElement | null>(null)
|
|
300
|
+
const paneHeight = ref<number | null>(null)
|
|
301
|
+
let paneFrame = 0
|
|
302
|
+
|
|
303
|
+
function measurePanes(): void {
|
|
304
|
+
cancelAnimationFrame(paneFrame)
|
|
305
|
+
paneFrame = requestAnimationFrame(() => {
|
|
306
|
+
const element = panes.value
|
|
307
|
+
paneHeight.value =
|
|
308
|
+
element && window.matchMedia(SIDE_BY_SIDE_QUERY).matches
|
|
309
|
+
? measureFillHeight(element)
|
|
310
|
+
: null
|
|
311
|
+
})
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// What sits above or below the panes (a banner, the sampling note) moves
|
|
315
|
+
// them: the body's size says so on a page that scrolls, this block's own on
|
|
316
|
+
// a shell that scrolls a panel. Their own height leaves the answer as is,
|
|
317
|
+
// so the observer settles at once.
|
|
318
|
+
let paneObserver: ResizeObserver | null = null
|
|
319
|
+
onMounted(() => {
|
|
320
|
+
measurePanes()
|
|
321
|
+
window.addEventListener('resize', measurePanes)
|
|
322
|
+
paneObserver = new ResizeObserver(measurePanes)
|
|
323
|
+
paneObserver.observe(document.body)
|
|
324
|
+
const block = panes.value?.parentElement
|
|
325
|
+
if (block) {
|
|
326
|
+
paneObserver.observe(block)
|
|
327
|
+
}
|
|
328
|
+
})
|
|
329
|
+
onBeforeUnmount(() => {
|
|
330
|
+
cancelAnimationFrame(paneFrame)
|
|
331
|
+
window.removeEventListener('resize', measurePanes)
|
|
332
|
+
paneObserver?.disconnect()
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
// The panes come and go with the waiting state.
|
|
336
|
+
watch(panes, (element) => element && measurePanes())
|
|
337
|
+
|
|
338
|
+
const paneStyle = computed(() =>
|
|
339
|
+
paneHeight.value === null ? undefined : { height: `${paneHeight.value}px` },
|
|
340
|
+
)
|
|
341
|
+
|
|
194
342
|
/** Stacked under the list (below xl), the preview is out of sight: bring it in. */
|
|
195
343
|
async function pick(path: string): Promise<void> {
|
|
196
344
|
selectPath(path, 'push')
|
|
@@ -216,10 +364,17 @@ function inspectTyped(): void {
|
|
|
216
364
|
*/
|
|
217
365
|
const selectedPage = ref<MarketingPageEntry>()
|
|
218
366
|
watch([pages, selectedPath], ([list, path]) => {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
367
|
+
selectedPage.value = selectedPageRow(list, path, selectedPage.value)
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
/** What the list is read for, the filter aside: a change is another read. */
|
|
371
|
+
const readScope = computed(() =>
|
|
372
|
+
JSON.stringify([workspace.value, pagesPeriod.value]),
|
|
373
|
+
)
|
|
374
|
+
// Another workspace or other days: the row read before says nothing of
|
|
375
|
+
// them, the header waits for the path's row in the new list (or none).
|
|
376
|
+
watch(readScope, () => {
|
|
377
|
+
selectedPage.value = undefined
|
|
223
378
|
})
|
|
224
379
|
|
|
225
380
|
/** Funnels with a page step on the selected path, for the "Used in" card. */
|
|
@@ -232,11 +387,17 @@ const usedIn = computed(() =>
|
|
|
232
387
|
)
|
|
233
388
|
|
|
234
389
|
// The preview draws in the browser only (frames, canvases): a server render
|
|
235
|
-
// holds its place.
|
|
236
|
-
|
|
390
|
+
// holds its place, until the browser hydrating it is mounted. A client
|
|
391
|
+
// navigation draws it with the list, from the first render: mounted later,
|
|
392
|
+
// it would come in after the list, and its skeleton after none.
|
|
393
|
+
const mounted = ref(!import.meta.env.SSR && !isDmsHydrating())
|
|
394
|
+
if (mounted.value) {
|
|
395
|
+
selectFirst(inventory.value)
|
|
396
|
+
}
|
|
237
397
|
onMounted(() => {
|
|
238
398
|
mounted.value = true
|
|
239
399
|
selectFirst(inventory.value)
|
|
400
|
+
wantedWorkspace.value ||= rememberedWorkspace()
|
|
240
401
|
})
|
|
241
402
|
|
|
242
403
|
const countLabel = computed(() => {
|
|
@@ -251,53 +412,6 @@ const sortItems = computed(() => [
|
|
|
251
412
|
{ value: 'views', label: t('page.marketing.pages.views') },
|
|
252
413
|
{ value: 'clicks', label: t('page.marketing.pages.overlay.clicks') },
|
|
253
414
|
])
|
|
254
|
-
|
|
255
|
-
async function resetSite(): Promise<void> {
|
|
256
|
-
const site = website.value
|
|
257
|
-
if (!site) {
|
|
258
|
-
return
|
|
259
|
-
}
|
|
260
|
-
const counts = await countSiteHeatmap().catch(() => null)
|
|
261
|
-
await confirm({
|
|
262
|
-
title: t('page.marketing.pages.reset.site_title', { name: site.name }),
|
|
263
|
-
description: t('page.marketing.pages.reset.site_description'),
|
|
264
|
-
color: 'error',
|
|
265
|
-
icon: 'i-ph-eraser',
|
|
266
|
-
confirmLabel:
|
|
267
|
-
counts && counts.events + counts.snapshots > 0
|
|
268
|
-
? t('page.marketing.pages.reset.confirm_count', {
|
|
269
|
-
what: heatmapDataLabel(counts, t, locale.value),
|
|
270
|
-
})
|
|
271
|
-
: t('page.marketing.pages.reset.confirm'),
|
|
272
|
-
confirmText: site.domain,
|
|
273
|
-
// A hostname is not case-sensitive: neither is the typed check.
|
|
274
|
-
confirmTextCaseInsensitive: true,
|
|
275
|
-
impact: heatmapResetImpact(counts, t, locale.value),
|
|
276
|
-
onConfirm: async () => {
|
|
277
|
-
const deleted = await api.resetHeatmap(site._id)
|
|
278
|
-
toast.add({
|
|
279
|
-
color: 'success',
|
|
280
|
-
title: t('page.marketing.pages.reset.site_done', { name: site.name }),
|
|
281
|
-
description: t('page.marketing.pages.reset.done', {
|
|
282
|
-
what: heatmapDataLabel(deleted, t, locale.value),
|
|
283
|
-
}),
|
|
284
|
-
})
|
|
285
|
-
// The dialog closes once the pages show the emptied counts.
|
|
286
|
-
await refreshPageBlocks()
|
|
287
|
-
},
|
|
288
|
-
})
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
const moreItems = computed(() => [
|
|
292
|
-
[
|
|
293
|
-
{
|
|
294
|
-
label: t('page.marketing.pages.reset.site_button'),
|
|
295
|
-
icon: 'i-ph-eraser',
|
|
296
|
-
color: 'error' as const,
|
|
297
|
-
onSelect: () => void resetSite(),
|
|
298
|
-
},
|
|
299
|
-
],
|
|
300
|
-
])
|
|
301
415
|
</script>
|
|
302
416
|
|
|
303
417
|
<template>
|
|
@@ -336,10 +450,27 @@ const moreItems = computed(() => [
|
|
|
336
450
|
:description="t('page.marketing.pages.tracking_off.description')"
|
|
337
451
|
/>
|
|
338
452
|
|
|
339
|
-
<div
|
|
453
|
+
<div
|
|
454
|
+
ref="panes"
|
|
455
|
+
class="grid items-start gap-4 xl:grid-cols-[22rem_minmax(0,1fr)]"
|
|
456
|
+
>
|
|
340
457
|
<section
|
|
341
|
-
class="dms-card flex min-w-0 flex-col overflow-hidden p-0
|
|
458
|
+
class="dms-card flex min-w-0 flex-col overflow-hidden p-0"
|
|
459
|
+
:class="paneHeight === null ? 'xl:sticky xl:top-4' : ''"
|
|
460
|
+
:style="paneStyle"
|
|
342
461
|
>
|
|
462
|
+
<!-- Whose dashboard the list reads, above its search: read with
|
|
463
|
+
the page, so the row is there from the first paint. -->
|
|
464
|
+
<div
|
|
465
|
+
v-if="workspaceOptions.length > 1"
|
|
466
|
+
class="border-b border-default p-3"
|
|
467
|
+
>
|
|
468
|
+
<DmsMarketingWorkspaceScope
|
|
469
|
+
:workspaces="workspaceOptions"
|
|
470
|
+
:model-value="workspace"
|
|
471
|
+
@update:model-value="selectWorkspace"
|
|
472
|
+
/>
|
|
473
|
+
</div>
|
|
343
474
|
<div class="flex items-center gap-2 border-b border-default p-3">
|
|
344
475
|
<UInput
|
|
345
476
|
v-model="search"
|
|
@@ -349,15 +480,6 @@ const moreItems = computed(() => [
|
|
|
349
480
|
:placeholder="t('page.marketing.pages.search_placeholder')"
|
|
350
481
|
@keyup.enter="inspectTyped"
|
|
351
482
|
/>
|
|
352
|
-
<UDropdownMenu :items="moreItems" :content="{ align: 'end' }">
|
|
353
|
-
<UButton
|
|
354
|
-
icon="i-ph-dots-three"
|
|
355
|
-
size="sm"
|
|
356
|
-
color="neutral"
|
|
357
|
-
variant="ghost"
|
|
358
|
-
:aria-label="t('page.marketing.pages.more_list')"
|
|
359
|
-
/>
|
|
360
|
-
</UDropdownMenu>
|
|
361
483
|
</div>
|
|
362
484
|
<div class="flex items-center justify-between gap-2 px-4 py-2">
|
|
363
485
|
<DmsEyebrow :label="countLabel" />
|
|
@@ -429,10 +551,15 @@ const moreItems = computed(() => [
|
|
|
429
551
|
/>
|
|
430
552
|
<ul
|
|
431
553
|
v-else
|
|
432
|
-
class="flex
|
|
433
|
-
:class="
|
|
554
|
+
class="flex flex-col gap-1 overflow-y-auto p-2"
|
|
555
|
+
:class="[
|
|
556
|
+
loading ? 'opacity-60' : '',
|
|
557
|
+
paneHeight === null
|
|
558
|
+
? 'max-h-[45vh] xl:max-h-[70vh]'
|
|
559
|
+
: 'min-h-0 flex-1',
|
|
560
|
+
]"
|
|
434
561
|
>
|
|
435
|
-
<li v-for="page in
|
|
562
|
+
<li v-for="page in rows" :key="page.path">
|
|
436
563
|
<button
|
|
437
564
|
type="button"
|
|
438
565
|
class="flex w-full flex-col gap-1 rounded-lg border px-3 py-2 text-left transition-colors"
|
|
@@ -488,6 +615,20 @@ const moreItems = computed(() => [
|
|
|
488
615
|
}}
|
|
489
616
|
</template>
|
|
490
617
|
</span>
|
|
618
|
+
<span
|
|
619
|
+
v-if="page.badge"
|
|
620
|
+
class="flex shrink-0 items-center gap-1 text-[11px]"
|
|
621
|
+
:class="page.badge.muted ? 'text-dimmed' : 'text-muted'"
|
|
622
|
+
:title="page.badge.title"
|
|
623
|
+
>
|
|
624
|
+
<UIcon
|
|
625
|
+
:name="page.badge.icon"
|
|
626
|
+
class="size-3"
|
|
627
|
+
:class="page.badge.muted ? '' : 'text-success'"
|
|
628
|
+
/>
|
|
629
|
+
<span class="sr-only">{{ page.badge.title }}</span>
|
|
630
|
+
<span aria-hidden="true">{{ page.badge.label }}</span>
|
|
631
|
+
</span>
|
|
491
632
|
<span
|
|
492
633
|
class="h-1 w-14 overflow-hidden rounded-full bg-elevated"
|
|
493
634
|
aria-hidden="true"
|
|
@@ -525,6 +666,7 @@ const moreItems = computed(() => [
|
|
|
525
666
|
<USkeleton
|
|
526
667
|
v-if="!mounted || !selectedPath || !website"
|
|
527
668
|
class="h-[32rem] rounded-xl"
|
|
669
|
+
:style="paneStyle"
|
|
528
670
|
aria-hidden="true"
|
|
529
671
|
/>
|
|
530
672
|
<DmsMarketingPagePreview
|
|
@@ -536,6 +678,9 @@ const moreItems = computed(() => [
|
|
|
536
678
|
:tracker-enabled="trackerEnabled"
|
|
537
679
|
:pageviews="selectedPage?.pageviews"
|
|
538
680
|
:used-in="usedIn"
|
|
681
|
+
:height="paneHeight"
|
|
682
|
+
:workspace="workspace"
|
|
683
|
+
:workspace-snapshots="inventory?.snapshotsEnabled ?? false"
|
|
539
684
|
@website-updated="putWebsite"
|
|
540
685
|
/>
|
|
541
686
|
</div>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed, ref
|
|
2
|
+
import { computed, ref } from 'vue'
|
|
3
3
|
import { useI18n } from '#dms/frontend-module'
|
|
4
4
|
import { useContextLink } from '../composables/useContextLink'
|
|
5
5
|
import { displayName, formatNumber, percentChangeTrend } from '../utils/format'
|
|
@@ -60,7 +60,10 @@ const props = withDefaults(
|
|
|
60
60
|
)
|
|
61
61
|
|
|
62
62
|
const HIGHLIGHTED_RANKS = 3
|
|
63
|
-
|
|
63
|
+
/** The rows a tab lists at most (the tops route's limit): the skeleton's. */
|
|
64
|
+
const SKELETON_ROWS = 8
|
|
65
|
+
/** The tabs the overview groups hold at most: the skeleton's. */
|
|
66
|
+
const SKELETON_TABS = 3
|
|
64
67
|
const RANK_DIGITS = 2
|
|
65
68
|
|
|
66
69
|
const { t, te, locale } = useI18n()
|
|
@@ -73,21 +76,27 @@ const { data, isLoading, error, refresh } = useChartFetch<TopTabsResponse>({
|
|
|
73
76
|
})
|
|
74
77
|
|
|
75
78
|
const tabs = computed(() => data.value?.tabs ?? [])
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
79
|
+
/** The picked tab; the first one until a pick names one the data has. */
|
|
80
|
+
const pickedId = ref<string>('')
|
|
81
|
+
/**
|
|
82
|
+
* Derived rather than set by a watcher: a server render runs no watcher once
|
|
83
|
+
* its data lands, and would draw the empty state the browser then replaces.
|
|
84
|
+
*/
|
|
85
|
+
const activeId = computed({
|
|
86
|
+
get: () =>
|
|
87
|
+
tabs.value.some((tab) => tab.id === pickedId.value)
|
|
88
|
+
? pickedId.value
|
|
89
|
+
: (tabs.value[0]?.id ?? ''),
|
|
90
|
+
set: (id: string) => {
|
|
91
|
+
pickedId.value = id
|
|
84
92
|
},
|
|
85
|
-
|
|
86
|
-
)
|
|
93
|
+
})
|
|
87
94
|
|
|
88
95
|
const active = computed(
|
|
89
96
|
() => tabs.value.find((tab) => tab.id === activeId.value) ?? null,
|
|
90
97
|
)
|
|
98
|
+
/** Nothing to draw yet: the skeleton, the size of a full card. */
|
|
99
|
+
const pending = computed(() => !data.value && !error.value && isLoading.value)
|
|
91
100
|
const maxShare = computed(() =>
|
|
92
101
|
Math.max(1, ...(active.value?.items.map((item) => item.share) ?? [])),
|
|
93
102
|
)
|
|
@@ -174,6 +183,16 @@ const TABS_UI = {
|
|
|
174
183
|
class="w-full"
|
|
175
184
|
/>
|
|
176
185
|
</div>
|
|
186
|
+
<!-- The tab strip's place while the tabs load, at its height. -->
|
|
187
|
+
<div
|
|
188
|
+
v-else-if="pending"
|
|
189
|
+
class="order-last -mx-1 flex min-w-0 basis-full gap-1 overflow-hidden sm:order-none sm:mx-0 sm:flex-1 sm:basis-0"
|
|
190
|
+
aria-hidden="true"
|
|
191
|
+
>
|
|
192
|
+
<span v-for="index in SKELETON_TABS" :key="index" class="px-2 py-3">
|
|
193
|
+
<USkeleton class="h-3.5 w-24" />
|
|
194
|
+
</span>
|
|
195
|
+
</div>
|
|
177
196
|
<DmsEyebrow
|
|
178
197
|
:label="processI18n(valueLabel)"
|
|
179
198
|
class="ms-auto shrink-0 py-3"
|
|
@@ -204,19 +223,27 @@ const TABS_UI = {
|
|
|
204
223
|
/>
|
|
205
224
|
</template>
|
|
206
225
|
</DmsEmptyState>
|
|
226
|
+
<!-- Each row at a list row's size: rank, label over its share bar, count
|
|
227
|
+
and share. -->
|
|
207
228
|
<ul
|
|
208
|
-
v-else-if="
|
|
229
|
+
v-else-if="pending"
|
|
209
230
|
class="divide-y divide-default"
|
|
210
231
|
aria-hidden="true"
|
|
211
232
|
>
|
|
212
233
|
<li
|
|
213
234
|
v-for="index in SKELETON_ROWS"
|
|
214
235
|
:key="index"
|
|
215
|
-
class="
|
|
236
|
+
class="grid grid-cols-[1.5rem_minmax(0,1fr)_auto_3rem] items-center gap-x-3 px-4 py-2"
|
|
216
237
|
>
|
|
217
|
-
<USkeleton class="h-3 w-
|
|
218
|
-
<
|
|
219
|
-
|
|
238
|
+
<USkeleton class="h-3 w-4" />
|
|
239
|
+
<span class="min-w-0">
|
|
240
|
+
<span class="flex h-[19.5px] items-center">
|
|
241
|
+
<USkeleton class="h-3 w-3/5" />
|
|
242
|
+
</span>
|
|
243
|
+
<USkeleton class="mt-1 h-0.5 w-2/5" />
|
|
244
|
+
</span>
|
|
245
|
+
<USkeleton class="h-3 w-8" />
|
|
246
|
+
<USkeleton class="ms-auto h-3 w-9" />
|
|
220
247
|
</li>
|
|
221
248
|
</ul>
|
|
222
249
|
<p
|
|
@@ -230,7 +257,13 @@ const TABS_UI = {
|
|
|
230
257
|
class="divide-y divide-default transition-opacity"
|
|
231
258
|
:class="isLoading ? 'opacity-60' : ''"
|
|
232
259
|
>
|
|
233
|
-
|
|
260
|
+
<!-- A row's height, kept while its link component loads on a first
|
|
261
|
+
navigation (the divider adds a pixel to every row but the first). -->
|
|
262
|
+
<li
|
|
263
|
+
v-for="(item, index) in active.items"
|
|
264
|
+
:key="item.id"
|
|
265
|
+
class="min-h-[42.5px] first:min-h-[41.5px]"
|
|
266
|
+
>
|
|
234
267
|
<component
|
|
235
268
|
:is="item.to ? 'DmsAutoLink' : 'div'"
|
|
236
269
|
:to="item.to ? inContext(item.to) : undefined"
|