@antelopejs/dms-marketing 0.4.1 → 0.5.0
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/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/routes/public-endpoints.d.ts +2 -0
- package/dist/routes/public-endpoints.js +14 -0
- package/dist/routes/public-endpoints.js.map +1 -0
- package/frontend-vue/app/components/Context.vue +37 -61
- package/frontend-vue/app/components/FunnelBuilder.vue +22 -19
- package/frontend-vue/app/components/FunnelTemplates.vue +16 -27
- package/frontend-vue/app/components/InstallGuide.vue +40 -57
- package/frontend-vue/app/components/PagePreview.vue +60 -76
- package/frontend-vue/app/components/PagesExplorer.vue +86 -80
- package/frontend-vue/app/components/WebsitesGrid.vue +32 -23
- package/frontend-vue/app/composables/useMarketingApi.ts +7 -120
- package/frontend-vue/app/composables/useMarketingContext.ts +1 -10
- package/frontend-vue/app/composables/useMarketingNow.ts +29 -0
- package/frontend-vue/app/composables/useMarketingReads.ts +314 -0
- package/frontend-vue/app/utils/format.ts +37 -1
- package/frontend-vue/package.json +1 -1
- package/frontend-vue/tests/format.test.ts +21 -0
- package/frontend-vue/tests/marketing-reads.test.ts +252 -0
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -52,6 +52,7 @@ const page_1 = require("@antelopejs/interface-dms/page");
|
|
|
52
52
|
const node_cron_1 = __importDefault(require("node-cron"));
|
|
53
53
|
const config_1 = require("./config");
|
|
54
54
|
const hooks_2 = require("./hooks");
|
|
55
|
+
const public_endpoints_1 = require("./routes/public-endpoints");
|
|
55
56
|
const services_1 = require("./services");
|
|
56
57
|
const constants_1 = require("./types/constants");
|
|
57
58
|
const HOURLY_CRON = "0 * * * *";
|
|
@@ -69,6 +70,7 @@ async function construct(config) {
|
|
|
69
70
|
sourcePath: node_path_1.default.join(__dirname, "../frontend-vue"),
|
|
70
71
|
renderer: { name: "vue", version: "3" },
|
|
71
72
|
priority: 0,
|
|
73
|
+
publicEndpoints: public_endpoints_1.TRACKER_PUBLIC_ENDPOINTS,
|
|
72
74
|
});
|
|
73
75
|
}
|
|
74
76
|
async function onDatabaseInitialized() {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BA,8BAsBC;AAiCD,0BAAkC;AAElC,oBAWC;AAlGD,6BAA8C;AAC9C,sBAAoB;AACpB,oBAAkB;AAClB,mBAAiB;AAEjB,0DAA6B;AAC7B,+DAAgE;AAChE,gEAA6D;AAC7D,6FAAqE;AACrE,2DAAqE;AACrE,yDAAmE;AACnE,0DAAqD;AACrD,qCAA8D;AAC9D,mCAAyD;AACzD,gEAAqE;AACrE,yCAKoB;AACpB,iDAAwE;AAExE,MAAM,WAAW,GAAG,WAAW,CAAC;AAEhC,MAAM,kBAAkB,GAAG,EAAE,GAAG,yBAAa,CAAC;AAE9C,IAAI,SAAS,GAAyB,IAAI,CAAC;AAC3C,IAAI,cAAc,GAA0B,IAAI,CAAC;AAE1C,KAAK,UAAU,SAAS,CAC7B,MAAoC;IAEpC,IAAA,kBAAS,EAAC,MAAM,CAAC,CAAC;IAClB,MAAM,IAAA,4BAAiB,GAAE,CAAC;IAC1B,IAAA,sCAA8B,GAAE,CAAC;IACjC,IAAA,oBAAY,EAAC,YAAI,CAAC,oBAAoB,EAAE,qBAAqB,CAAC,CAAC;IAI/D,IAAA,mCAAkB,EAChB,wDAAa,qCAAqC,GAAC,EACnD,wDAAa,iCAAiC,GAAC,CAChD,CAAC;IAEF,MAAM,IAAA,wBAAiB,EAAC;QACtB,IAAI,EAAE,gCAAoB;QAC1B,UAAU,EAAE,mBAAI,CAAC,IAAI,CAAC,SAAS,EAAE,iBAAiB,CAAC;QACnD,QAAQ,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE;QACvC,QAAQ,EAAE,CAAC;QACX,eAAe,EAAE,2CAAwB;KAC1C,CAAC,CAAC;AACL,CAAC;AAQD,KAAK,UAAU,qBAAqB;IAClC,MAAM,QAAQ,GAAG,IAAA,wCAAQ,EAAC,2BAAsB,CAAC,CAAC;IAClD,MAAM,IAAA,uBAAY,EAAC,QAAQ,CAAC,CAAC;IAI7B,cAAc,KAAK,WAAW,CAAC,GAAG,EAAE;QAClC,KAAK,IAAA,yBAAc,EAAC,QAAQ,CAAC,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC;IACtD,CAAC,EAAE,kBAAkB,CAAC,CAAC;IACvB,cAAc,CAAC,KAAK,EAAE,CAAC;IACvB,SAAS,KAAK,mBAAI,CAAC,QAAQ,CAAC,WAAW,EAAE,GAAG,EAAE;QAC5C,KAAK,IAAA,yBAAc,EAAC,QAAQ,CAAC;aAC1B,IAAI,CAAC,GAAG,EAAE,CAAC,IAAA,0BAAe,EAAC,aAAa,CAAC,CAAC;aAC1C,KAAK,CAAC,aAAa,CAAC,CAAC;IAC1B,CAAC,CAAC,CAAC;AACL,CAAC;AAED,SAAS,aAAa,CAAC,GAAY;IACjC,iBAAO,CAAC,KAAK,CAAC,yCAAyC,EAAE,GAAG,CAAC,CAAC;AAChE,CAAC;AAED,SAAS,cAAc,CAAC,GAAY;IAClC,iBAAO,CAAC,KAAK,CAAC,yCAAyC,EAAE,GAAG,CAAC,CAAC;AAChE,CAAC;AAED,SAAgB,OAAO,KAAU,CAAC;AAElC,SAAgB,IAAI;IAClB,IAAI,cAAc,EAAE,CAAC;QACnB,aAAa,CAAC,cAAc,CAAC,CAAC;QAC9B,cAAc,GAAG,IAAI,CAAC;IACxB,CAAC;IACD,IAAI,SAAS,EAAE,CAAC;QAGd,KAAK,SAAS,CAAC,IAAI,EAAE,CAAC;QACtB,SAAS,GAAG,IAAI,CAAC;IACnB,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TRACKER_PUBLIC_ENDPOINTS = void 0;
|
|
4
|
+
const constants_1 = require("@/types/constants");
|
|
5
|
+
const SCRIPT_METHODS = ["GET", "HEAD"];
|
|
6
|
+
const BEACON_METHODS = ["POST"];
|
|
7
|
+
exports.TRACKER_PUBLIC_ENDPOINTS = [
|
|
8
|
+
{ path: `${constants_1.API_BASE_PATH}/tracker.js`, methods: SCRIPT_METHODS },
|
|
9
|
+
{ path: `${constants_1.API_BASE_PATH}/experiments.js`, methods: SCRIPT_METHODS },
|
|
10
|
+
{ path: `${constants_1.API_BASE_PATH}/snapshot.js`, methods: SCRIPT_METHODS },
|
|
11
|
+
{ path: `${constants_1.API_BASE_PATH}/collect`, methods: BEACON_METHODS },
|
|
12
|
+
{ path: `${constants_1.API_BASE_PATH}/snapshot`, methods: BEACON_METHODS },
|
|
13
|
+
];
|
|
14
|
+
//# sourceMappingURL=public-endpoints.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"public-endpoints.js","sourceRoot":"","sources":["../../src/routes/public-endpoints.ts"],"names":[],"mappings":";;;AACA,iDAAkD;AAElD,MAAM,cAAc,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;AACvC,MAAM,cAAc,GAAG,CAAC,MAAM,CAAC,CAAC;AAQnB,QAAA,wBAAwB,GAA6B;IAChE,EAAE,IAAI,EAAE,GAAG,yBAAa,aAAa,EAAE,OAAO,EAAE,cAAc,EAAE;IAChE,EAAE,IAAI,EAAE,GAAG,yBAAa,iBAAiB,EAAE,OAAO,EAAE,cAAc,EAAE;IACpE,EAAE,IAAI,EAAE,GAAG,yBAAa,cAAc,EAAE,OAAO,EAAE,cAAc,EAAE;IACjE,EAAE,IAAI,EAAE,GAAG,yBAAa,UAAU,EAAE,OAAO,EAAE,cAAc,EAAE;IAC7D,EAAE,IAAI,EAAE,GAAG,yBAAa,WAAW,EAAE,OAAO,EAAE,cAAc,EAAE;CAC/D,CAAC"}
|
|
@@ -4,11 +4,12 @@ import { useDmsRoute, useDmsRouter, useI18n } from '#dms/frontend-module'
|
|
|
4
4
|
import {
|
|
5
5
|
MARKETING_CONTEXT_KEY,
|
|
6
6
|
type ContextWebsite,
|
|
7
|
-
type MarketingContextPayload,
|
|
8
7
|
} from '../composables/useMarketingContext'
|
|
9
8
|
import { useMarketingApi } from '../composables/useMarketingApi'
|
|
9
|
+
import { useMarketingContextRead } from '../composables/useMarketingReads'
|
|
10
|
+
import { useMarketingNow } from '../composables/useMarketingNow'
|
|
10
11
|
import { installLink } from '../composables/useMarketingRoutes'
|
|
11
|
-
import { formatRelativeTime, initials } from '../utils/format'
|
|
12
|
+
import { formatRelativeTime, initials, utcDayStart } from '../utils/format'
|
|
12
13
|
import { MAX_PERIOD_DAYS, MS_PER_DAY, PERIOD_SCOPE } from '../constants'
|
|
13
14
|
|
|
14
15
|
/**
|
|
@@ -30,7 +31,9 @@ import { MAX_PERIOD_DAYS, MS_PER_DAY, PERIOD_SCOPE } from '../constants'
|
|
|
30
31
|
* Its children render once the URL names one of the tenant's websites; with
|
|
31
32
|
* no website at all the first-run hero takes their place, and with
|
|
32
33
|
* `waitingGuide` a website still waiting for its first event shows the way
|
|
33
|
-
* to its install guide instead.
|
|
34
|
+
* to its install guide instead. A server render reads the context first: a
|
|
35
|
+
* URL naming no website (or one the tenant does not have) is redirected to
|
|
36
|
+
* the one saved, and the page renders on it.
|
|
34
37
|
*/
|
|
35
38
|
const props = withDefaults(
|
|
36
39
|
defineProps<{
|
|
@@ -88,11 +91,22 @@ const comparisonLabels = computed(() =>
|
|
|
88
91
|
),
|
|
89
92
|
)
|
|
90
93
|
|
|
91
|
-
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
+
// The minute's activity of every website: read on the bar's own clock.
|
|
95
|
+
const contextRead = useMarketingContextRead({
|
|
96
|
+
refreshInterval: LIVE_REFRESH_MS,
|
|
97
|
+
})
|
|
98
|
+
const context = contextRead.data
|
|
99
|
+
/**
|
|
100
|
+
* The context could not be read, and none is shown: a failed re-read (the
|
|
101
|
+
* minute's activity, a retry) keeps the bar as it is.
|
|
102
|
+
*/
|
|
103
|
+
const failed = computed(
|
|
104
|
+
() =>
|
|
105
|
+
!!contextRead.error.value &&
|
|
106
|
+
contextRead.status.value !== 'pending' &&
|
|
107
|
+
!context.value,
|
|
108
|
+
)
|
|
94
109
|
const switching = ref(false)
|
|
95
|
-
const refreshToken = ref(0)
|
|
96
110
|
|
|
97
111
|
const websites = computed(() => context.value?.websites ?? [])
|
|
98
112
|
/** The URL's website, while it is one of the tenant's. */
|
|
@@ -109,26 +123,8 @@ const selected = computed<ContextWebsite | null>(
|
|
|
109
123
|
() => websites.value.find((site) => site.id === selectedId.value) ?? null,
|
|
110
124
|
)
|
|
111
125
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
failed.value = false
|
|
115
|
-
try {
|
|
116
|
-
context.value = await api.getContext()
|
|
117
|
-
} catch {
|
|
118
|
-
failed.value = true
|
|
119
|
-
} finally {
|
|
120
|
-
loading.value = false
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/** The websites' last events again, quietly: a failure keeps the bar as is. */
|
|
125
|
-
async function refreshActivity(): Promise<void> {
|
|
126
|
-
try {
|
|
127
|
-
context.value = await api.getContext()
|
|
128
|
-
} catch {
|
|
129
|
-
// The next refresh tries again.
|
|
130
|
-
}
|
|
131
|
-
}
|
|
126
|
+
/** The websites and their last events again; a failure keeps the bar as is. */
|
|
127
|
+
const reload = (): Promise<void> => contextRead.refresh()
|
|
132
128
|
|
|
133
129
|
async function select(id: string): Promise<void> {
|
|
134
130
|
if (id === selectedId.value) {
|
|
@@ -153,8 +149,10 @@ async function select(id: string): Promise<void> {
|
|
|
153
149
|
}
|
|
154
150
|
|
|
155
151
|
// The URL names no website, or one the tenant does not have: fill in the
|
|
156
|
-
// saved one, without a history entry
|
|
157
|
-
// rewriting the URL for its own state
|
|
152
|
+
// saved one, without a history entry (a server render redirects to it).
|
|
153
|
+
// Watched with the query too, so a block rewriting the URL for its own state
|
|
154
|
+
// (the heatmap path) keeps it. Set up once the context is read.
|
|
155
|
+
await contextRead
|
|
158
156
|
watch(
|
|
159
157
|
[selectedId, () => route.query],
|
|
160
158
|
([id]) => {
|
|
@@ -165,8 +163,6 @@ watch(
|
|
|
165
163
|
{ immediate: true },
|
|
166
164
|
)
|
|
167
165
|
|
|
168
|
-
watch(refreshToken, () => refreshPageBlocks())
|
|
169
|
-
|
|
170
166
|
// --- Period --------------------------------------------------------------------
|
|
171
167
|
|
|
172
168
|
const period = usePeriodScope(props.periodScope)
|
|
@@ -190,52 +186,40 @@ const cutHint = computed(() => {
|
|
|
190
186
|
provide(MARKETING_CONTEXT_KEY, {
|
|
191
187
|
websites,
|
|
192
188
|
selected,
|
|
193
|
-
refreshToken,
|
|
194
189
|
select,
|
|
195
190
|
reload,
|
|
196
191
|
})
|
|
197
192
|
|
|
198
|
-
onMounted(reload)
|
|
199
|
-
|
|
200
193
|
// --- Clock -------------------------------------------------------------------
|
|
201
194
|
// A period reaching today follows the clock: its blocks read again every
|
|
202
195
|
// minute, through the page's live gate. The selector itself moves a day
|
|
203
196
|
// preset on at midnight.
|
|
204
197
|
|
|
205
|
-
const now =
|
|
198
|
+
const now = useMarketingNow(CLOCK_TICK_MS)
|
|
206
199
|
|
|
207
200
|
/** The period covers the present: its counts are still moving. */
|
|
208
201
|
const reachesToday = computed(
|
|
209
202
|
() => !!range.value && range.value.to.getTime() >= now.value,
|
|
210
203
|
)
|
|
211
204
|
|
|
212
|
-
/** Refresh: every block reads again. */
|
|
205
|
+
/** Refresh: every block reads again, and the bar. */
|
|
213
206
|
function refresh(): void {
|
|
214
|
-
|
|
215
|
-
void
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function tick(): void {
|
|
219
|
-
now.value = Date.now()
|
|
207
|
+
refreshPageBlocks()
|
|
208
|
+
void reload()
|
|
220
209
|
}
|
|
221
210
|
|
|
211
|
+
/** A period reaching today: its blocks read again, through the live gate. */
|
|
222
212
|
function liveRefresh(): void {
|
|
223
213
|
if (reachesToday.value && linkedId.value) {
|
|
224
214
|
refreshLivePageBlocks()
|
|
225
215
|
}
|
|
226
|
-
void refreshActivity()
|
|
227
216
|
}
|
|
228
217
|
|
|
229
|
-
let clock: ReturnType<typeof setInterval> | null = null
|
|
230
218
|
let live: ReturnType<typeof setInterval> | null = null
|
|
231
219
|
onMounted(() => {
|
|
232
|
-
clock = setInterval(tick, CLOCK_TICK_MS)
|
|
233
220
|
live = setInterval(liveRefresh, LIVE_REFRESH_MS)
|
|
234
221
|
})
|
|
235
222
|
onBeforeUnmount(() => {
|
|
236
|
-
if (clock) {
|
|
237
|
-
clearInterval(clock)
|
|
238
|
-
}
|
|
239
223
|
if (live) {
|
|
240
224
|
clearInterval(live)
|
|
241
225
|
}
|
|
@@ -298,18 +282,10 @@ const websiteItems = computed(() => [
|
|
|
298
282
|
* UTC's, the bar says when their days really start.
|
|
299
283
|
*/
|
|
300
284
|
const utcHint = computed(() => {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
}
|
|
305
|
-
const utcMidnight = new Date(
|
|
306
|
-
Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate()),
|
|
307
|
-
)
|
|
308
|
-
const time = new Intl.DateTimeFormat(locale.value, {
|
|
309
|
-
hour: '2-digit',
|
|
310
|
-
minute: '2-digit',
|
|
311
|
-
}).format(utcMidnight)
|
|
312
|
-
return t('page.marketing.context.utc_days', { time })
|
|
285
|
+
// The reader's zone: the browser's, which a server render reads from its
|
|
286
|
+
// cookie.
|
|
287
|
+
const time = utcDayStart(now.value, locale.value, readerTimeZone())
|
|
288
|
+
return time ? t('page.marketing.context.utc_days', { time }) : ''
|
|
313
289
|
})
|
|
314
290
|
</script>
|
|
315
291
|
|
|
@@ -320,7 +296,7 @@ const utcHint = computed(() => {
|
|
|
320
296
|
role="toolbar"
|
|
321
297
|
:aria-label="t('page.marketing.context.label')"
|
|
322
298
|
>
|
|
323
|
-
<USkeleton v-if="
|
|
299
|
+
<USkeleton v-if="!context && !failed" class="h-7 w-72" />
|
|
324
300
|
<template v-else-if="selected">
|
|
325
301
|
<UDropdownMenu :items="websiteItems" :content="{ align: 'start' }">
|
|
326
302
|
<UButton
|
|
@@ -11,6 +11,11 @@ import {
|
|
|
11
11
|
type MarketingFunnelStep,
|
|
12
12
|
type MarketingFunnelSuggestions,
|
|
13
13
|
} from '../composables/useMarketingApi'
|
|
14
|
+
import {
|
|
15
|
+
useContextOnDemand,
|
|
16
|
+
useFunnelRead,
|
|
17
|
+
useFunnelSuggestions,
|
|
18
|
+
} from '../composables/useMarketingReads'
|
|
14
19
|
import type { ContextWebsite } from '../composables/useMarketingContext'
|
|
15
20
|
import {
|
|
16
21
|
funnelBuilderLink,
|
|
@@ -199,9 +204,13 @@ function clearVariationError(): void {
|
|
|
199
204
|
|
|
200
205
|
// --- Loading -------------------------------------------------------------------
|
|
201
206
|
|
|
207
|
+
// The definition edited, or the one a Duplicate copies.
|
|
208
|
+
const readDefinition = useFunnelRead(funnelId || sourceId)
|
|
209
|
+
const readContext = useContextOnDemand()
|
|
210
|
+
|
|
202
211
|
/** A stored definition as the form holds it. */
|
|
203
|
-
async function fetchDefinition(
|
|
204
|
-
const row = await
|
|
212
|
+
async function fetchDefinition() {
|
|
213
|
+
const row = await readDefinition()
|
|
205
214
|
const experiment = parseExperimentValue(row.experiment)
|
|
206
215
|
const draft: FunnelDraftValues = {
|
|
207
216
|
name: row.name,
|
|
@@ -230,7 +239,7 @@ function objectiveOf(draft: FunnelDraftValues): string {
|
|
|
230
239
|
}
|
|
231
240
|
|
|
232
241
|
async function loadExisting(): Promise<void> {
|
|
233
|
-
const { draft, experiment } = await fetchDefinition(
|
|
242
|
+
const { draft, experiment } = await fetchDefinition()
|
|
234
243
|
applyValues(draft)
|
|
235
244
|
original.value = experiment
|
|
236
245
|
savedObjective.value = objectiveOf(draft)
|
|
@@ -242,7 +251,7 @@ const copied = ref<FunnelDraftValues | null>(null)
|
|
|
242
251
|
/** Duplicate: the copy is a new draft, unsaved until the user saves it — its
|
|
243
252
|
* experiment key must change, keys being unique per site. */
|
|
244
253
|
async function loadCopy(): Promise<void> {
|
|
245
|
-
const { draft } = await fetchDefinition(
|
|
254
|
+
const { draft } = await fetchDefinition()
|
|
246
255
|
applyValues({
|
|
247
256
|
...draft,
|
|
248
257
|
name: t('page.marketing.builder.copy_name', { name: draft.name }).slice(
|
|
@@ -260,7 +269,7 @@ async function loadCopy(): Promise<void> {
|
|
|
260
269
|
const websites = ref<ContextWebsite[]>([])
|
|
261
270
|
|
|
262
271
|
async function loadWebsites(): Promise<void> {
|
|
263
|
-
const payload = await
|
|
272
|
+
const payload = await readContext()
|
|
264
273
|
websites.value = payload.websites
|
|
265
274
|
const linked = queryParam('website')
|
|
266
275
|
websiteId.value ||=
|
|
@@ -301,21 +310,15 @@ onMounted(load)
|
|
|
301
310
|
|
|
302
311
|
// --- Suggestions and preview -------------------------------------------------
|
|
303
312
|
|
|
304
|
-
const
|
|
313
|
+
const NO_SUGGESTIONS: MarketingFunnelSuggestions = { pages: [], events: [] }
|
|
305
314
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
suggestions.value = { pages: [], events: [] }
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
// Fires once the site is known (the context, or the funnel being edited).
|
|
318
|
-
watch(websiteId, () => void loadSuggestions())
|
|
315
|
+
// Read once the site is known (the context, or the funnel being edited).
|
|
316
|
+
const suggestionsRead = useFunnelSuggestions(websiteId)
|
|
317
|
+
const suggestions = computed(() =>
|
|
318
|
+
suggestionsRead.error.value
|
|
319
|
+
? NO_SUGGESTIONS
|
|
320
|
+
: (suggestionsRead.data.value ?? NO_SUGGESTIONS),
|
|
321
|
+
)
|
|
319
322
|
|
|
320
323
|
const focusedStep = ref<number | null>(null)
|
|
321
324
|
/** The step whose list Escape closed, until it is typed in again. */
|
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed
|
|
2
|
+
import { computed } from 'vue'
|
|
3
3
|
import { useDmsRouter, useI18n } from '#dms/frontend-module'
|
|
4
4
|
import { useMarketingContext } from '../composables/useMarketingContext'
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
type MarketingFunnelStep,
|
|
8
|
-
type MarketingFunnelTemplate,
|
|
9
|
-
} from '../composables/useMarketingApi'
|
|
5
|
+
import type { MarketingFunnelStep } from '../composables/useMarketingApi'
|
|
6
|
+
import { useFunnelTemplates } from '../composables/useMarketingReads'
|
|
10
7
|
import {
|
|
11
8
|
funnelBuilderLink,
|
|
12
9
|
installLink,
|
|
@@ -31,31 +28,23 @@ void props
|
|
|
31
28
|
const { t, locale } = useI18n()
|
|
32
29
|
const inContext = useContextLink()
|
|
33
30
|
const router = useDmsRouter()
|
|
34
|
-
const api = useMarketingApi()
|
|
35
31
|
const context = useMarketingContext()
|
|
36
32
|
const period = usePeriodScope(PERIOD_SCOPE)
|
|
37
33
|
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
[
|
|
42
|
-
() => context.selected.value?.id,
|
|
43
|
-
() => period.value?.range?.from.getTime(),
|
|
44
|
-
() => period.value?.range?.to.getTime(),
|
|
45
|
-
],
|
|
46
|
-
async ([website]) => {
|
|
47
|
-
if (!website) {
|
|
48
|
-
return
|
|
49
|
-
}
|
|
50
|
-
templates.value = null
|
|
51
|
-
try {
|
|
52
|
-
templates.value = await api.getFunnelTemplates(website, period.value)
|
|
53
|
-
} catch {
|
|
54
|
-
templates.value = []
|
|
55
|
-
}
|
|
56
|
-
},
|
|
57
|
-
{ immediate: true },
|
|
34
|
+
const templatesRead = useFunnelTemplates(
|
|
35
|
+
() => context.selected.value?.id,
|
|
36
|
+
period,
|
|
58
37
|
)
|
|
38
|
+
/**
|
|
39
|
+
* The site's templates; `null` while they load (the previous site's or
|
|
40
|
+
* period's included), none once the read failed.
|
|
41
|
+
*/
|
|
42
|
+
const templates = computed(() => {
|
|
43
|
+
if (templatesRead.error.value && templatesRead.status.value !== 'pending') {
|
|
44
|
+
return []
|
|
45
|
+
}
|
|
46
|
+
return templatesRead.placeholder.value ? null : templatesRead.data.value
|
|
47
|
+
})
|
|
59
48
|
|
|
60
49
|
/** The site has sent nothing yet: templates cannot come before the tracker. */
|
|
61
50
|
const waiting = computed(() => context.selected.value?.state === 'waiting')
|
|
@@ -1,24 +1,19 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed,
|
|
2
|
+
import { computed, onMounted, ref, watch } from 'vue'
|
|
3
3
|
import {
|
|
4
4
|
useDmsRoute,
|
|
5
5
|
useDmsRouter,
|
|
6
6
|
useDmsRuntimeConfig,
|
|
7
7
|
useI18n,
|
|
8
8
|
} from '#dms/frontend-module'
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
type MarketingConnectionStatus,
|
|
12
|
-
} from '../composables/useMarketingApi'
|
|
9
|
+
import { useMarketingApi } from '../composables/useMarketingApi'
|
|
10
|
+
import { useWebsiteConnection } from '../composables/useMarketingReads'
|
|
13
11
|
import {
|
|
14
12
|
installLink,
|
|
15
13
|
overviewLink,
|
|
16
14
|
websitesLink,
|
|
17
15
|
} from '../composables/useMarketingRoutes'
|
|
18
|
-
import {
|
|
19
|
-
allowHostConfirmation,
|
|
20
|
-
hostConflictOf,
|
|
21
|
-
} from '../utils/host-conflict'
|
|
16
|
+
import { allowHostConfirmation, hostConflictOf } from '../utils/host-conflict'
|
|
22
17
|
import { trackerSnippet } from '../utils/snippet'
|
|
23
18
|
|
|
24
19
|
/**
|
|
@@ -57,12 +52,20 @@ const fieldErrors = useFieldErrors({
|
|
|
57
52
|
const websiteId = ref(
|
|
58
53
|
typeof route.query.website === 'string' ? route.query.website : '',
|
|
59
54
|
)
|
|
60
|
-
const status = ref<MarketingConnectionStatus | null>(null)
|
|
61
|
-
const failed = ref(false)
|
|
62
|
-
/** The `?website=` names no site of the workspace (deleted, mistyped). */
|
|
63
|
-
const missing = ref(false)
|
|
64
55
|
/** Server time "Check again" waits from; null: since the site's creation. */
|
|
65
56
|
const recheckSince = ref<number | null>(null)
|
|
57
|
+
/** The check's polling period: `POLL_MS` while it waits for a pageview, 0 (stopped) otherwise. */
|
|
58
|
+
const pollInterval = ref(0)
|
|
59
|
+
const connection = useWebsiteConnection(websiteId, recheckSince, {
|
|
60
|
+
refreshInterval: pollInterval,
|
|
61
|
+
})
|
|
62
|
+
const status = connection.data
|
|
63
|
+
/** The `?website=` names no site of the workspace (deleted, mistyped). */
|
|
64
|
+
const missing = computed(
|
|
65
|
+
() => apiErrorStatus(connection.error.value) === HTTP_NOT_FOUND,
|
|
66
|
+
)
|
|
67
|
+
/** The last check failed; the next one is on its way. */
|
|
68
|
+
const failed = computed(() => !!connection.error.value && !missing.value)
|
|
66
69
|
|
|
67
70
|
// --- Step 1 ----------------------------------------------------------------------
|
|
68
71
|
|
|
@@ -95,7 +98,6 @@ async function createWebsite(): Promise<void> {
|
|
|
95
98
|
await api.selectContextWebsite(created._id)
|
|
96
99
|
websiteId.value = created._id
|
|
97
100
|
await router.replace(installLink(created._id))
|
|
98
|
-
await poll()
|
|
99
101
|
} catch (error) {
|
|
100
102
|
const conflict = hostConflictOf(error)
|
|
101
103
|
if (conflict) {
|
|
@@ -115,47 +117,27 @@ async function createWebsite(): Promise<void> {
|
|
|
115
117
|
|
|
116
118
|
// --- Step 3: connection check ---------------------------------------------------
|
|
117
119
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
try {
|
|
133
|
-
status.value = await api.getConnection(
|
|
134
|
-
websiteId.value,
|
|
135
|
-
recheckSince.value ?? undefined,
|
|
136
|
-
)
|
|
137
|
-
failed.value = false
|
|
138
|
-
} catch (error) {
|
|
139
|
-
if (apiErrorStatus(error) === HTTP_NOT_FOUND) {
|
|
140
|
-
missing.value = true
|
|
141
|
-
status.value = null
|
|
142
|
-
return
|
|
143
|
-
}
|
|
144
|
-
failed.value = true
|
|
145
|
-
}
|
|
146
|
-
if (!status.value?.visit) {
|
|
147
|
-
timer = setTimeout(() => void poll(), POLL_MS)
|
|
148
|
-
}
|
|
149
|
-
}
|
|
120
|
+
/** No pageview yet, and the site is there: the check polls. */
|
|
121
|
+
const isWaiting = computed(
|
|
122
|
+
() => !!websiteId.value && !missing.value && !status.value?.visit,
|
|
123
|
+
)
|
|
124
|
+
// The check polls while it waits, and stops once a pageview arrived or the
|
|
125
|
+
// site is gone.
|
|
126
|
+
watch(
|
|
127
|
+
isWaiting,
|
|
128
|
+
(waiting) => {
|
|
129
|
+
pollInterval.value = waiting ? POLL_MS : 0
|
|
130
|
+
},
|
|
131
|
+
{ immediate: true },
|
|
132
|
+
)
|
|
150
133
|
|
|
151
134
|
/** Waits for a pageview sent from now on, to re-verify an installed tag. */
|
|
152
|
-
|
|
135
|
+
function checkAgain(): void {
|
|
153
136
|
if (!status.value) {
|
|
154
137
|
return
|
|
155
138
|
}
|
|
156
139
|
recheckSince.value = status.value.checkedAt
|
|
157
140
|
status.value = { ...status.value, visit: null }
|
|
158
|
-
await poll()
|
|
159
141
|
}
|
|
160
142
|
|
|
161
143
|
const apiOrigin = ref('')
|
|
@@ -164,9 +146,7 @@ onMounted(() => {
|
|
|
164
146
|
apiOrigin.value = String(
|
|
165
147
|
(config.public as { dms?: { baseURL?: string } }).dms?.baseURL ?? '',
|
|
166
148
|
)
|
|
167
|
-
void poll()
|
|
168
149
|
})
|
|
169
|
-
onBeforeUnmount(stopPolling)
|
|
170
150
|
|
|
171
151
|
watch(
|
|
172
152
|
() => route.query.website,
|
|
@@ -174,10 +154,8 @@ watch(
|
|
|
174
154
|
const next = typeof value === 'string' ? value : ''
|
|
175
155
|
if (next !== websiteId.value) {
|
|
176
156
|
websiteId.value = next
|
|
177
|
-
status.value = null
|
|
178
|
-
missing.value = false
|
|
179
157
|
recheckSince.value = null
|
|
180
|
-
|
|
158
|
+
connection.clear()
|
|
181
159
|
}
|
|
182
160
|
},
|
|
183
161
|
)
|
|
@@ -201,7 +179,8 @@ async function allow(hostname: string): Promise<void> {
|
|
|
201
179
|
color: 'success',
|
|
202
180
|
title: t('page.marketing.install.allowed', { host: hostname }),
|
|
203
181
|
})
|
|
204
|
-
|
|
182
|
+
// The refused host leaves the check before its button comes back.
|
|
183
|
+
await refreshPageBlocks()
|
|
205
184
|
} catch (error) {
|
|
206
185
|
const conflict = hostConflictOf(error)
|
|
207
186
|
if (conflict) {
|
|
@@ -232,7 +211,7 @@ async function resumeTracking(): Promise<void> {
|
|
|
232
211
|
color: 'success',
|
|
233
212
|
title: t('page.marketing.websites.resumed'),
|
|
234
213
|
})
|
|
235
|
-
await
|
|
214
|
+
await refreshPageBlocks()
|
|
236
215
|
} catch (error) {
|
|
237
216
|
fieldErrors.toastError(error, {
|
|
238
217
|
toastTitle: 'page.marketing.websites.save_error',
|
|
@@ -638,7 +617,9 @@ const stepTwoState = computed(() =>
|
|
|
638
617
|
/>
|
|
639
618
|
</span>
|
|
640
619
|
<span class="min-w-0 flex-1">
|
|
641
|
-
<span
|
|
620
|
+
<span
|
|
621
|
+
class="block font-semibold break-words text-highlighted"
|
|
622
|
+
>
|
|
642
623
|
{{
|
|
643
624
|
t(
|
|
644
625
|
recheckSince === null
|
|
@@ -677,7 +658,9 @@ const stepTwoState = computed(() =>
|
|
|
677
658
|
>
|
|
678
659
|
<DmsIconWell icon="i-ph-prohibit" tone="error" />
|
|
679
660
|
<span class="min-w-0 flex-1">
|
|
680
|
-
<span
|
|
661
|
+
<span
|
|
662
|
+
class="block font-semibold break-words text-highlighted"
|
|
663
|
+
>
|
|
681
664
|
{{
|
|
682
665
|
t(
|
|
683
666
|
'page.marketing.install.refused',
|