@picked/svelte 0.0.1-dev.1793776638554 → 0.0.1-dev.1793923852884
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/.tsbuildinfo +1 -1
- package/dist/experience/set-picked-context.js +1 -1
- package/dist/plugins/devtools/DevToolsExperience.svelte +155 -80
- package/dist/plugins/devtools/customTools.d.ts +2 -2
- package/dist/plugins/devtools/derive/answer-display/answer-display.d.ts +14 -0
- package/dist/plugins/devtools/derive/answer-display/answer-display.js +1 -1
- package/dist/plugins/devtools/derive/answer-display/index.d.ts +2 -2
- package/dist/plugins/devtools/derive/answer-display/index.js +1 -1
- package/dist/plugins/devtools/derive/guide-sections/guide-sections.js +1 -1
- package/dist/plugins/devtools/derive/index.d.ts +5 -5
- package/dist/plugins/devtools/derive/index.js +1 -1
- package/dist/plugins/devtools/derive/personas/index.d.ts +2 -2
- package/dist/plugins/devtools/derive/personas/index.js +1 -1
- package/dist/plugins/devtools/derive/personas/personas.d.ts +88 -7
- package/dist/plugins/devtools/derive/personas/personas.js +1 -1
- package/dist/plugins/devtools/derive/plain-language/index.d.ts +1 -1
- package/dist/plugins/devtools/derive/plain-language/index.js +1 -1
- package/dist/plugins/devtools/derive/plain-language/plain-language.d.ts +5 -5
- package/dist/plugins/devtools/derive/plain-language/plain-language.js +1 -1
- package/dist/plugins/devtools/index.d.ts +1 -1
- package/dist/plugins/devtools/launcher/Launcher.svelte +13 -4
- package/dist/plugins/devtools/options.d.ts +6 -4
- package/dist/plugins/devtools/options.js +1 -1
- package/dist/plugins/devtools/review/journey/ExperienceDetails.svelte +129 -0
- package/dist/plugins/devtools/review/journey/JourneyPanel.svelte +54 -8
- package/dist/plugins/devtools/review/personas/ChoiceButton.svelte +40 -6
- package/dist/plugins/devtools/review/personas/PersonaBuilder.svelte +69 -45
- package/dist/plugins/devtools/review/personas/PersonaCard.svelte +226 -43
- package/dist/plugins/devtools/review/personas/PersonasPanel.svelte +171 -74
- package/dist/plugins/devtools/review/personas/load-persona.d.ts +41 -20
- package/dist/plugins/devtools/review/personas/load-persona.js +1 -1
- package/dist/plugins/devtools/review/personas/persona-route.d.ts +87 -0
- package/dist/plugins/devtools/review/personas/persona-route.js +1 -0
- package/dist/plugins/devtools/review/personas/persona-session.d.ts +94 -0
- package/dist/plugins/devtools/review/personas/persona-session.js +1 -0
- package/dist/plugins/devtools/sidebar/Sidebar.svelte +14 -48
- package/dist/plugins/devtools/sidebar/SidebarRow.svelte +2 -2
- package/dist/plugins/devtools/sidebar/nav.d.ts +26 -39
- package/dist/plugins/devtools/sidebar/nav.js +1 -1
- package/package.json +2 -2
- package/dist/plugins/devtools/screens/experience-instances/ExperienceInstancesListScreen.svelte +0 -10
- package/dist/plugins/devtools/screens/experiences/ExperienceDetailsScreen.svelte +0 -44
- package/dist/plugins/devtools/screens/experiences/ExperienceExperienceInstancesListScreen.svelte +0 -13
- package/dist/plugins/devtools/screens/experiences/ExperienceScreen.svelte +0 -32
- package/dist/plugins/devtools/screens/experiences/ExperienceTreeScreen.svelte +0 -500
- package/dist/plugins/devtools/screens/experiences/ExperiencesListScreen.svelte +0 -22
- package/dist/plugins/devtools/screens/features/FeaturesListScreen.svelte +0 -239
- package/dist/plugins/devtools/screens/responses/ResponseTableRow.svelte +0 -286
- package/dist/plugins/devtools/screens/responses/ResponsesListScreen.svelte +0 -12
- package/dist/plugins/devtools/screens/responses/ResponsesTable.svelte +0 -88
|
@@ -1,22 +1,29 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
/**
|
|
3
3
|
* @file The Personas & Scenarios tab: customers and situations to try the journey as. The site's
|
|
4
|
-
* own come from the plugin's `personas` option. A reviewer makes theirs in
|
|
5
|
-
* answering the journey's questions here, screen by screen, or from where
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* own come from the plugin's `personas` option, under their headings. A reviewer makes theirs in
|
|
5
|
+
* this browser, either by answering the journey's questions here, screen by screen, or from where
|
|
6
|
+
* they are on the page, and can edit any persona; an edit of the site's own stays in this
|
|
7
|
+
* browser. A persona keeps the answers a customer gives on fields they can see, the screens moved
|
|
8
|
+
* on from, the screen it stops on and the page it starts on.
|
|
9
|
+
*
|
|
10
|
+
* Walk through reloads that page clean and pre-fills every step with the persona's responses, so
|
|
11
|
+
* the reviewer moves through it themselves. Jump to step walks the persona to a step on its
|
|
12
|
+
* route first. Site tools placed here follow, each a section under its label.
|
|
10
13
|
*/
|
|
11
14
|
import type { FieldValue, Picked } from '@picked/client'
|
|
12
15
|
import { getStepFields } from '@picked/client/form'
|
|
16
|
+
import { SvelteMap } from 'svelte/reactivity'
|
|
13
17
|
import { Banner, Button, Card, EmptyState, RadioGroup, Stack, Text, TextField } from '../../ui'
|
|
14
18
|
import {
|
|
19
|
+
applyEdits,
|
|
15
20
|
buildPersona,
|
|
16
21
|
customerAnswers,
|
|
17
22
|
deslug,
|
|
18
23
|
entryUrlOf,
|
|
24
|
+
groupPersonas,
|
|
19
25
|
isBlank,
|
|
26
|
+
matchesSearch,
|
|
20
27
|
passedScreens,
|
|
21
28
|
readSavedPersonas,
|
|
22
29
|
writeSavedPersonas,
|
|
@@ -32,6 +39,8 @@
|
|
|
32
39
|
import type { CustomTool } from '../../customTools'
|
|
33
40
|
import type { JourneyView } from '../../session'
|
|
34
41
|
import { loadUrl, startPersonaLoad } from './load-persona'
|
|
42
|
+
import { personaLink } from './persona-session'
|
|
43
|
+
import { personaRoute, routeScreens, type RouteScreen } from './persona-route'
|
|
35
44
|
import PersonaBuilder from './PersonaBuilder.svelte'
|
|
36
45
|
import PersonaCard from './PersonaCard.svelte'
|
|
37
46
|
|
|
@@ -50,14 +59,22 @@
|
|
|
50
59
|
hiddenKeys: ReadonlySet<string>
|
|
51
60
|
/** Keys a screen carries only for its code. */
|
|
52
61
|
codeOwned: ReadonlySet<string>
|
|
53
|
-
/**
|
|
62
|
+
/**
|
|
63
|
+
* Screens that move on by themselves, responses every walk gives, and actions that open
|
|
64
|
+
* another journey.
|
|
65
|
+
*/
|
|
54
66
|
personaWalk: {
|
|
55
67
|
selfAdvancingSteps: ReadonlySet<string>
|
|
68
|
+
responses: Readonly<Record<string, FieldValue>>
|
|
56
69
|
handoffs: Readonly<Record<string, string>>
|
|
57
70
|
}
|
|
58
71
|
/** The site's tools placed in this tab. */
|
|
59
72
|
tools?: readonly CustomTool[]
|
|
60
|
-
/**
|
|
73
|
+
/** The persona this tab walks through as; `null` outside a persona session. */
|
|
74
|
+
activePersona: Persona | null
|
|
75
|
+
/** Ends the persona session. */
|
|
76
|
+
onStop: () => void
|
|
77
|
+
/** How the last jump's walk ended; `null` when no jump ran on this page. */
|
|
61
78
|
walkNotice: Notice | null
|
|
62
79
|
onWalkNoticeDismiss: () => void
|
|
63
80
|
/** The storage saved personas live in. */
|
|
@@ -89,6 +106,8 @@
|
|
|
89
106
|
codeOwned,
|
|
90
107
|
personaWalk,
|
|
91
108
|
tools = [],
|
|
109
|
+
activePersona,
|
|
110
|
+
onStop,
|
|
92
111
|
walkNotice,
|
|
93
112
|
onWalkNoticeDismiss,
|
|
94
113
|
getStorage = () => window.localStorage,
|
|
@@ -129,26 +148,65 @@
|
|
|
129
148
|
let buildChoices = $state.raw<Record<string, boolean>>({})
|
|
130
149
|
let buildWalk = $state.raw<BuilderWalk | null>(null)
|
|
131
150
|
let notice = $state<Notice | null>(null)
|
|
132
|
-
let
|
|
151
|
+
let startingId = $state<string | null>(null)
|
|
152
|
+
let search = $state('')
|
|
133
153
|
/** The answers Save would keep; `null` while they are read. */
|
|
134
154
|
let captured = $state<Record<string, FieldValue> | null>(null)
|
|
135
155
|
|
|
156
|
+
const listed = $derived(applyEdits(personas, saved))
|
|
157
|
+
const siteGroups = $derived(
|
|
158
|
+
groupPersonas(
|
|
159
|
+
listed.site.filter((persona) => matchesSearch(persona, search)),
|
|
160
|
+
'For this site',
|
|
161
|
+
),
|
|
162
|
+
)
|
|
163
|
+
const own = $derived(listed.own.filter((persona) => matchesSearch(persona, search)))
|
|
164
|
+
/** Every heading in the list: the site's groups, then this browser's own. */
|
|
165
|
+
const sections = $derived(
|
|
166
|
+
own.length > 0 ? [...siteGroups, { title: 'Saved in this browser', personas: own }] : siteGroups,
|
|
167
|
+
)
|
|
136
168
|
const isEmpty = $derived(personas.length === 0 && saved.length === 0 && tools.length === 0)
|
|
169
|
+
/** Enough personas that a reviewer needs to search them. */
|
|
170
|
+
const searchable = $derived(personas.length + listed.own.length > 6)
|
|
137
171
|
const postureReason = $derived(posture.canEditAnswers ? undefined : (posture.reason ?? undefined))
|
|
172
|
+
const routeSettings = $derived({
|
|
173
|
+
asked,
|
|
174
|
+
codeOwned,
|
|
175
|
+
selfAdvancing: personaWalk.selfAdvancingSteps,
|
|
176
|
+
handoffs: personaWalk.handoffs,
|
|
177
|
+
hiddenKeys,
|
|
178
|
+
responses: personaWalk.responses,
|
|
179
|
+
})
|
|
180
|
+
/** Each persona's route as last worked out, so a jump moves on from its screens. */
|
|
181
|
+
const routes = new SvelteMap<string, BuilderWalk>()
|
|
138
182
|
|
|
139
183
|
/**
|
|
140
|
-
* Why a persona
|
|
184
|
+
* Why starting a persona is withheld: the session's posture, or another persona starting.
|
|
141
185
|
*
|
|
142
186
|
* @param {Persona} persona - The persona on the card.
|
|
143
|
-
* @returns {string | undefined} The reason, or `undefined` while
|
|
144
|
-
* @example
|
|
187
|
+
* @returns {string | undefined} The reason, or `undefined` while it can start.
|
|
188
|
+
* @example startReason(persona)
|
|
145
189
|
*/
|
|
146
|
-
function
|
|
190
|
+
function startReason(persona: Persona): string | undefined {
|
|
147
191
|
if (postureReason !== undefined) return postureReason
|
|
148
|
-
return
|
|
149
|
-
? 'Another persona is
|
|
192
|
+
return startingId !== null && startingId !== persona.id
|
|
193
|
+
? 'Another persona is starting.'
|
|
150
194
|
: undefined
|
|
151
195
|
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* The screens a persona's responses take it through.
|
|
199
|
+
*
|
|
200
|
+
* @param {Persona} persona - The persona.
|
|
201
|
+
* @returns {Promise<RouteScreen[] | null>} The screens, or `null` when it has no journey.
|
|
202
|
+
* @example await routeOf(persona)
|
|
203
|
+
*/
|
|
204
|
+
async function routeOf(persona: Persona): Promise<RouteScreen[] | null> {
|
|
205
|
+
const walk = await personaRoute(client, persona, routeSettings, window.location)
|
|
206
|
+
if (walk === null) return null
|
|
207
|
+
routes.set(persona.id, walk)
|
|
208
|
+
return routeScreens(walk, persona.landing)
|
|
209
|
+
}
|
|
152
210
|
const landing = $derived<PersonaLanding | null>(
|
|
153
211
|
journey === null
|
|
154
212
|
? null
|
|
@@ -166,7 +224,7 @@
|
|
|
166
224
|
const stop = buildWalk.landing
|
|
167
225
|
if (stop === null) return `Saves ${answers}.`
|
|
168
226
|
const screen = stop.stepName ?? deslug(stop.stepKey ?? '')
|
|
169
|
-
return `Saves ${answers}.
|
|
227
|
+
return `Saves ${answers}. It stops at ${screen}.`
|
|
170
228
|
})
|
|
171
229
|
|
|
172
230
|
const captureSummary = $derived.by(() => {
|
|
@@ -174,11 +232,11 @@
|
|
|
174
232
|
const count = Object.keys(captured).length
|
|
175
233
|
const answers = count === 1 ? '1 response' : `${count} responses`
|
|
176
234
|
if (landing === null)
|
|
177
|
-
return `Saves ${answers}. No experience is open, so it
|
|
235
|
+
return `Saves ${answers}. No experience is open, so it keeps the responses only.`
|
|
178
236
|
const journeyName = landing.experienceName ?? deslug(landing.experienceKey)
|
|
179
|
-
if (landing.stepKey === null) return `Saves ${answers}.
|
|
237
|
+
if (landing.stepKey === null) return `Saves ${answers}. It starts in ${journeyName}.`
|
|
180
238
|
const screen = landing.stepName ?? deslug(landing.stepKey)
|
|
181
|
-
return `Saves ${answers}.
|
|
239
|
+
return `Saves ${answers}. It stops at ${screen} in ${journeyName}.`
|
|
182
240
|
})
|
|
183
241
|
|
|
184
242
|
/**
|
|
@@ -355,7 +413,7 @@
|
|
|
355
413
|
}
|
|
356
414
|
|
|
357
415
|
const edited = editingId !== null
|
|
358
|
-
saved =
|
|
416
|
+
saved = saved.some((entry) => entry.id === result.persona.id)
|
|
359
417
|
? saved.map((entry) => (entry.id === result.persona.id ? result.persona : entry))
|
|
360
418
|
: [...saved, result.persona]
|
|
361
419
|
creating = false
|
|
@@ -370,56 +428,69 @@
|
|
|
370
428
|
}
|
|
371
429
|
|
|
372
430
|
/**
|
|
373
|
-
* Deletes a saved persona.
|
|
431
|
+
* Deletes a saved persona, or drops this browser's edit of a site persona.
|
|
374
432
|
*
|
|
375
|
-
* @param {Persona} persona - The persona
|
|
433
|
+
* @param {Persona} persona - The persona.
|
|
376
434
|
* @example remove(persona)
|
|
377
435
|
*/
|
|
378
436
|
function remove(persona: Persona): void {
|
|
379
437
|
saved = saved.filter((entry) => entry.id !== persona.id)
|
|
380
438
|
writeSavedPersonas(getStorage, client.projectId, saved)
|
|
381
|
-
notice = {
|
|
439
|
+
notice = {
|
|
440
|
+
status: 'success',
|
|
441
|
+
text: persona.edited ? `Put ${persona.name} back as the site has it.` : `Deleted ${persona.name}.`,
|
|
442
|
+
}
|
|
382
443
|
}
|
|
383
444
|
|
|
384
445
|
/**
|
|
385
|
-
*
|
|
386
|
-
*
|
|
446
|
+
* Starts a persona session: a clean reload of the persona's page that pre-fills every step, after
|
|
447
|
+
* walking to `stop` when one is picked.
|
|
387
448
|
*
|
|
388
|
-
* @param {Persona} persona - The persona
|
|
389
|
-
* @
|
|
390
|
-
* @example
|
|
449
|
+
* @param {Persona} persona - The persona.
|
|
450
|
+
* @param {RouteScreen | null} stop - The step to walk to; `null` for a walk-through from the start.
|
|
451
|
+
* @example start(persona, null)
|
|
391
452
|
*/
|
|
392
|
-
|
|
393
|
-
|
|
453
|
+
function start(persona: Persona, stop: RouteScreen | null): void {
|
|
454
|
+
startingId = persona.id
|
|
394
455
|
notice = null
|
|
395
456
|
onWalkNoticeDismiss()
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
notice = { status: 'error', text: `${persona.name} did not load.${detail}` }
|
|
417
|
-
loadingId = null
|
|
457
|
+
const started = startPersonaLoad(
|
|
458
|
+
client,
|
|
459
|
+
{
|
|
460
|
+
id: persona.id,
|
|
461
|
+
stop:
|
|
462
|
+
stop === null
|
|
463
|
+
? null
|
|
464
|
+
: {
|
|
465
|
+
experienceKey: stop.experienceKey,
|
|
466
|
+
experienceName: stop.experienceName,
|
|
467
|
+
stepKey: stop.stepKey,
|
|
468
|
+
stepName: stop.stepName,
|
|
469
|
+
},
|
|
470
|
+
passed: stop === null ? [] : (routes.get(persona.id)?.passed ?? []),
|
|
471
|
+
},
|
|
472
|
+
{ storage: getSessionStorage(), url: loadUrl(persona.entryUrl, window.location), navigate },
|
|
473
|
+
)
|
|
474
|
+
if (!started) {
|
|
475
|
+
notice = { status: 'error', text: 'This browser would not let the persona start.' }
|
|
476
|
+
startingId = null
|
|
418
477
|
}
|
|
419
478
|
}
|
|
420
479
|
</script>
|
|
421
480
|
|
|
422
481
|
<div class="personas" data-testid="devtools-personas">
|
|
482
|
+
{#if activePersona}
|
|
483
|
+
<Banner status="info" testId="devtools-persona-session">
|
|
484
|
+
<div class="session">
|
|
485
|
+
<span>
|
|
486
|
+
Walking through as <strong>{activePersona.name}</strong>. Each step shows their
|
|
487
|
+
responses. Click Continue, or change a response first.
|
|
488
|
+
</span>
|
|
489
|
+
<Button variant="secondary" size="small" onclick={onStop}>Stop</Button>
|
|
490
|
+
</div>
|
|
491
|
+
</Banner>
|
|
492
|
+
{/if}
|
|
493
|
+
|
|
423
494
|
{#if walkNotice}
|
|
424
495
|
<Banner
|
|
425
496
|
status={walkNotice.status}
|
|
@@ -439,6 +510,11 @@
|
|
|
439
510
|
<div class="form">
|
|
440
511
|
<Stack direction="vertical" gap="medium">
|
|
441
512
|
<Text weight="semibold">{editingId === null ? 'New persona' : 'Edit persona'}</Text>
|
|
513
|
+
{#if personas.some((persona) => persona.id === editingId)}
|
|
514
|
+
<Text size="small" color="subdued">
|
|
515
|
+
Your edits stay in this browser. Reset puts it back as the site has it.
|
|
516
|
+
</Text>
|
|
517
|
+
{/if}
|
|
442
518
|
<RadioGroup
|
|
443
519
|
label="Make it by"
|
|
444
520
|
name="persona-mode"
|
|
@@ -518,38 +594,46 @@
|
|
|
518
594
|
</div>
|
|
519
595
|
{:else}
|
|
520
596
|
<div class="toolbar">
|
|
597
|
+
{#if searchable}
|
|
598
|
+
<div class="search">
|
|
599
|
+
<TextField
|
|
600
|
+
label="Search"
|
|
601
|
+
name="persona-search"
|
|
602
|
+
placeholder="Search, such as moving or gas"
|
|
603
|
+
value={search}
|
|
604
|
+
oninput={(value) => (search = value)}
|
|
605
|
+
/>
|
|
606
|
+
</div>
|
|
607
|
+
{/if}
|
|
521
608
|
<Button variant="secondary" size="small" onclick={startCreate}>Create</Button>
|
|
522
609
|
</div>
|
|
523
610
|
{/if}
|
|
524
611
|
|
|
525
|
-
{#
|
|
526
|
-
<section class="group" aria-label=
|
|
527
|
-
<h3 class="group-title">
|
|
528
|
-
{#each personas as persona (persona.id)}
|
|
612
|
+
{#each sections as section (section.title)}
|
|
613
|
+
<section class="group" aria-label={section.title}>
|
|
614
|
+
<h3 class="group-title">{section.title}</h3>
|
|
615
|
+
{#each section.personas as persona (persona.id)}
|
|
529
616
|
<PersonaCard
|
|
530
617
|
{persona}
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
{/if}
|
|
538
|
-
|
|
539
|
-
{#if saved.length > 0}
|
|
540
|
-
<section class="group" aria-label="Saved in this browser">
|
|
541
|
-
<h3 class="group-title">Saved in this browser</h3>
|
|
542
|
-
{#each saved as persona (persona.id)}
|
|
543
|
-
<PersonaCard
|
|
544
|
-
{persona}
|
|
545
|
-
loadReason={loadReason(persona)}
|
|
546
|
-
loading={loadingId === persona.id}
|
|
547
|
-
onLoad={() => void load(persona)}
|
|
618
|
+
startReason={startReason(persona)}
|
|
619
|
+
starting={startingId === persona.id}
|
|
620
|
+
active={activePersona?.id === persona.id}
|
|
621
|
+
onWalkThrough={() => start(persona, null)}
|
|
622
|
+
route={() => routeOf(persona)}
|
|
623
|
+
onJump={(screen) => start(persona, screen)}
|
|
548
624
|
onEdit={() => startEdit(persona)}
|
|
549
|
-
onDelete={() => remove(persona)}
|
|
625
|
+
onDelete={persona.source === 'saved' ? () => remove(persona) : undefined}
|
|
626
|
+
onReset={persona.edited ? () => remove(persona) : undefined}
|
|
627
|
+
link={persona.source === 'site'
|
|
628
|
+
? personaLink(window.location, persona.entryUrl, persona.id)
|
|
629
|
+
: undefined}
|
|
550
630
|
/>
|
|
551
631
|
{/each}
|
|
552
632
|
</section>
|
|
633
|
+
{/each}
|
|
634
|
+
|
|
635
|
+
{#if search.length > 0 && sections.length === 0}
|
|
636
|
+
<Text size="small" color="subdued">No persona or scenario matches “{search}”.</Text>
|
|
553
637
|
{/if}
|
|
554
638
|
|
|
555
639
|
{#each tools as tool (tool.id)}
|
|
@@ -588,9 +672,22 @@
|
|
|
588
672
|
}
|
|
589
673
|
|
|
590
674
|
.toolbar {
|
|
675
|
+
align-items: flex-end;
|
|
591
676
|
justify-content: flex-end;
|
|
592
677
|
}
|
|
593
678
|
|
|
679
|
+
.search {
|
|
680
|
+
flex: 1 1 auto;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
.session {
|
|
684
|
+
display: flex;
|
|
685
|
+
flex-wrap: wrap;
|
|
686
|
+
align-items: center;
|
|
687
|
+
justify-content: space-between;
|
|
688
|
+
gap: var(--space-2);
|
|
689
|
+
}
|
|
690
|
+
|
|
594
691
|
.group {
|
|
595
692
|
display: flex;
|
|
596
693
|
flex-direction: column;
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @file
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* @file Starts a persona the way a customer arrives: the page reloads clean on the persona's entry
|
|
3
|
+
* page. A jump then walks the persona's journey from its first screen to the screen picked. Each
|
|
4
|
+
* screen gets its own answers as the walk reaches it, then Continue, so every condition and action
|
|
5
|
+
* between the screens runs for real.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
* answers and the journey history the SDK keeps, notes the persona in `sessionStorage` and
|
|
9
|
-
* reloads. The second
|
|
10
|
-
*
|
|
7
|
+
* A start has two halves on either side of the reload. The first, {@link startPersonaLoad}, clears
|
|
8
|
+
* the answers and the journey history the SDK keeps, notes the persona in `sessionStorage` and
|
|
9
|
+
* reloads. The second runs on the next page, once the devtools mount there and pick up the note:
|
|
10
|
+
* {@link walkPersona} for a jump, {@link openPersonaJourney} for a walk-through.
|
|
11
11
|
*
|
|
12
|
-
* The walk stops at the
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* The walk stops at the screen picked, or at the first screen it cannot finish: a question with no
|
|
13
|
+
* answer, an answer that fails its check, a screen that asks nothing and is not on the persona's
|
|
14
|
+
* route, or a Continue that lands nowhere.
|
|
15
15
|
*/
|
|
16
16
|
import type { FieldValue, Picked } from '@picked/client';
|
|
17
|
-
import { type Persona } from '../../derive';
|
|
17
|
+
import { type Persona, type PersonaLanding, type PersonaScreen } from '../../derive';
|
|
18
18
|
/** The part of an experience instance a walk reads and moves. */
|
|
19
19
|
export interface WalkInstance {
|
|
20
20
|
id: string;
|
|
@@ -142,6 +142,15 @@ export interface WalkOutcome {
|
|
|
142
142
|
}
|
|
143
143
|
/** The `sessionStorage` key that carries a load across the reload. */
|
|
144
144
|
export declare const PENDING_LOAD_KEY = "picked-devtools-persona-load";
|
|
145
|
+
/** A persona start, carried across its reload. */
|
|
146
|
+
export interface PendingLoad {
|
|
147
|
+
/** The persona's id. */
|
|
148
|
+
id: string;
|
|
149
|
+
/** The screen a jump walks to; `null` for a walk-through, which walks nowhere. */
|
|
150
|
+
stop: PersonaLanding | null;
|
|
151
|
+
/** The screens the walk moves on from on its way to `stop`, in order. */
|
|
152
|
+
passed: PersonaScreen[];
|
|
153
|
+
}
|
|
145
154
|
/** Where a load reloads, and how. */
|
|
146
155
|
export interface PersonaReload {
|
|
147
156
|
/** The tab's `sessionStorage`, which carries the load across the reload. */
|
|
@@ -166,16 +175,16 @@ export interface PersonaReload {
|
|
|
166
175
|
*/
|
|
167
176
|
export declare function screenQuestions(client: Picked, fieldKeys: readonly string[], codeOwned: ReadonlySet<string>): Promise<string[]>;
|
|
168
177
|
/**
|
|
169
|
-
* Clears what the session holds and reloads, so the next page can
|
|
170
|
-
*
|
|
178
|
+
* Clears what the session holds and reloads, so the next page can start the persona clean. The
|
|
179
|
+
* answers go, and so does the journey history, which a host may keep across reloads.
|
|
171
180
|
*
|
|
172
181
|
* @param {ResetClient} client - The client.
|
|
173
|
-
* @param {
|
|
182
|
+
* @param {PendingLoad} load - The persona, and the screen a jump walks to.
|
|
174
183
|
* @param {PersonaReload} reload - Where the load reloads, and how.
|
|
175
184
|
* @returns {boolean} `false` when the load could not be noted, so nothing was cleared.
|
|
176
|
-
* @example startPersonaLoad(client,
|
|
185
|
+
* @example startPersonaLoad(client, { id: persona.id, stop: null, passed: [] }, reload)
|
|
177
186
|
*/
|
|
178
|
-
export declare function startPersonaLoad(client: ResetClient,
|
|
187
|
+
export declare function startPersonaLoad(client: ResetClient, load: PendingLoad, reload: PersonaReload): boolean;
|
|
179
188
|
/**
|
|
180
189
|
* The URL a load goes to: the persona's entry page, or this page when it has none. This page's
|
|
181
190
|
* `picked-` parameters (a preview token, a devtools switch) ride along where the entry URL does not
|
|
@@ -193,13 +202,13 @@ export declare function loadUrl(entryUrl: string | null, here: {
|
|
|
193
202
|
search: string;
|
|
194
203
|
}): string;
|
|
195
204
|
/**
|
|
196
|
-
* Takes the
|
|
205
|
+
* Takes the start a reload was for off `sessionStorage`, so it runs once.
|
|
197
206
|
*
|
|
198
207
|
* @param {Pick<Storage, 'getItem' | 'removeItem'>} storage - The tab's `sessionStorage`.
|
|
199
|
-
* @returns {
|
|
208
|
+
* @returns {PendingLoad | null} The start, or `null` when none is waiting.
|
|
200
209
|
* @example takePendingLoad(sessionStorage)
|
|
201
210
|
*/
|
|
202
|
-
export declare function takePendingLoad(storage: Pick<Storage, 'getItem' | 'removeItem'>):
|
|
211
|
+
export declare function takePendingLoad(storage: Pick<Storage, 'getItem' | 'removeItem'>): PendingLoad | null;
|
|
203
212
|
/**
|
|
204
213
|
* Walks a persona's journey from its first screen: each screen gets the answers it asks, then
|
|
205
214
|
* Continue, until the persona's screen or the first screen the walk cannot finish.
|
|
@@ -212,6 +221,18 @@ export declare function takePendingLoad(storage: Pick<Storage, 'getItem' | 'remo
|
|
|
212
221
|
* @example await walkPersona(client, persona, settings)
|
|
213
222
|
*/
|
|
214
223
|
export declare function walkPersona(client: WalkClient, persona: Persona, settings: WalkSettings, timing?: WalkTiming): Promise<WalkOutcome>;
|
|
224
|
+
/**
|
|
225
|
+
* Gets a walk-through going once the page has started: the page's own experience when one is
|
|
226
|
+
* open, else the persona's first one.
|
|
227
|
+
*
|
|
228
|
+
* @param {WalkClient} client - The client, on a page that has just loaded.
|
|
229
|
+
* @param {Persona} persona - The persona.
|
|
230
|
+
* @param {WalkSettings} settings - Which experiences the devtools never follow.
|
|
231
|
+
* @param {WalkTiming} [timing] - How long to wait; tests pass a short one.
|
|
232
|
+
* @returns {Promise<boolean>} `false` when no experience is open at the end.
|
|
233
|
+
* @example await openPersonaJourney(client, persona, settings)
|
|
234
|
+
*/
|
|
235
|
+
export declare function openPersonaJourney(client: WalkClient, persona: Persona, settings: Pick<WalkSettings, 'exclude'>, timing?: WalkTiming): Promise<boolean>;
|
|
215
236
|
/**
|
|
216
237
|
* What the reviewer reads once a walk ends.
|
|
217
238
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{deslug as e,screenName as t}from"../../derive/plain-language/plain-language.js";import{
|
|
1
|
+
import{deslug as e,screenName as t}from"../../derive/plain-language/plain-language.js";import{readLanding as n,readPassed as r,startOf as i}from"../../derive/personas/personas.js";import{pickJourney as a}from"../../derive/journey-pick/journey-pick.js";import"../../derive/index.js";import{getStepFields as o}from"@picked/client/form";var s=`picked-devtools-persona-load`,c=30,l={sleep:e=>new Promise(t=>{setTimeout(t,e)}),bootTries:500,openTries:20,startTries:100,advanceTries:500,returnTries:33,selfAdvanceTries:1e3,maxHops:60};function u(e){return`${e.experienceKey}/${e.stepKey}`}async function d(e,t,n){for(let r=0;r<=t;r++){let i=e();if(i!==null)return i;r<t&&await n.sleep(c)}return null}function f(e,n,r){let i=e.experienceInstances.listStore.getState().filter(e=>n.has(e.experienceKey)),o=a(i,{exclude:r.exclude,precedence:r.precedence,openOrder:r.openOrder()}),s=o?.currentStepStore.getState();return o===void 0||s==null||o.currentNodeStore.getState()?.type!==`step`||o.stateStore.getState()!==`ready`?null:{instance:o,screen:{experienceKey:o.experienceKey,stepKey:s.key},fieldKeys:s.fieldKeys,stop:{experienceName:o.experienceName,stepName:t(s.name,s.key)}}}async function p(e,t,n){await e.openExperienceByKey(t),await d(()=>e.experienceInstances.listStore.getState().some(e=>e.experienceKey===t&&e.isOpenStore.getState())?!0:null,n.openTries,n)===null&&await e.launchExperienceByKey(t,{bypassCondition:!0,initialiseOpen:!0})}function m(e,t){let n={};for(let r of t)Object.hasOwn(e,r)&&(n[r]=e[r]);return n}async function h(e,t,n){let r=e.fields.listStore.getState(),i=t.flatMap(e=>r.filter(t=>t.key===e));return(await o(e,i)).map(e=>e.key).filter(e=>!n.has(e))}function g(e,t,n){try{n.storage.setItem(s,JSON.stringify(t))}catch{return!1}return e.responses.clearAll(),e.history.restore({entries:[]}),n.navigate(n.url),!0}function _(e,t){let n=new URL(e??`${t.pathname}${t.search}`,`https://entry.invalid`);for(let[e,r]of new URLSearchParams(t.search))e.startsWith(`picked-`)&&!n.searchParams.has(e)&&n.searchParams.append(e,r);return`${n.pathname}${n.search}`}function v(e){let t;try{t=JSON.parse(e)}catch{return null}if(typeof t!=`object`||!t||Array.isArray(t))return null;let i=Reflect.get(t,`id`);return typeof i!=`string`||i.length===0?null:{id:i,stop:n(Reflect.get(t,`stop`)),passed:r(Reflect.get(t,`passed`))}}function y(e){try{let t=e.getItem(s);return e.removeItem(s),t===null?null:v(t)}catch{return null}}function b(e,t){return e===null||e.experienceKey!==t.experienceKey?!1:e.stepKey===null||e.stepKey===t.stepKey}async function x(e,t,n){let r=!1,i=0;return await d(()=>{let n=e.resting();return n===null?(r=!0,i=0,null):u(n.screen)!==t||(r&&(i+=1),i>e.timing.returnTries?!1:null)},n,e.timing)===!0}async function S(e,t){let n=await e.settings.askedOn(t.fieldKeys),r=n.filter(t=>Object.hasOwn(e.answers,t));if(r.length>0){let{errors:n}=await e.client.responses.commit(m(e.answers,r));if(n.length>0)return{end:`invalid`,at:t.stop}}if(b(e.landing,t.screen))return{end:`arrived`,at:t.stop};let i=u(t.screen);return e.settings.selfAdvancing.has(t.screen.stepKey)?await x(e,i,e.timing.selfAdvanceTries)?null:{end:`stuck`,at:t.stop}:(n.length>0?r.length===n.length:e.passed.has(i))?(t.instance.goToNextStep(),await x(e,i,e.timing.advanceTries)?null:{end:`stuck`,at:t.stop}):{end:`needs-answer`,at:t.stop}}async function C(e,t,n,r=l){let{landing:a}=t,o=i(t);if(o===null)return Object.keys(t.responses).length>0&&e.responses.register({...t.responses}),{end:`answers-only`,at:null};let s=new Set([o,...t.passed.map(e=>e.experienceKey)]);a!==null&&s.add(a.experienceKey);let c={client:e,settings:n,timing:r,landing:a,passed:new Set(t.passed.map(u)),answers:{...t.responses,...n.responses},resting:()=>f(e,s,n)};await d(()=>e.readyStore.getState()?!0:null,r.bootTries,r),await d(c.resting,r.startTries,r)===null&&await p(e,o,r);for(let e=0;e<r.maxHops;e++){let e=await d(c.resting,r.startTries,r);if(e===null)return{end:`no-journey`,at:null};let t=await S(c,e);if(t!==null)return t}return{end:`hop-limit`,at:null}}async function w(e,t,n,r=l){let a=()=>e.experienceInstances.listStore.getState().some(e=>!n.exclude.has(e.experienceKey)&&e.isOpenStore.getState())?!0:null;if(await d(()=>e.readyStore.getState()?!0:null,r.bootTries,r),await d(a,r.startTries,r)!==null)return!0;let o=i(t);return o!==null&&(await p(e,o,r),await d(a,r.openTries,r)!==null)}function T(t,n){let{name:r}=t,a=n.at===null?``:`“${n.at.stepName}”`;switch(n.end){case`arrived`:return{status:`success`,text:`Jumped to ${a} as ${r}.`};case`answers-only`:return{status:`success`,text:`Loaded ${r}’s responses.`};case`needs-answer`:return{status:`warning`,text:`Walked as ${r} as far as ${a}. ${r} has no response for a field there.`};case`invalid`:return{status:`warning`,text:`Walked as ${r} as far as ${a}. A response there no longer passes its check.`};case`stuck`:return{status:`warning`,text:`Walked as ${r} as far as ${a}. The experience did not move on from there.`};case`no-journey`:{let n=i(t);return{status:`error`,text:`${r} did not load: ${t.landing?.experienceName??(n===null?`its experience`:e(n))} didn't open on this page.`}}case`hop-limit`:return{status:`warning`,text:`Stopped walking as ${r} after too many steps. The flow may be going round in a loop.`}}}export{s as PENDING_LOAD_KEY,_ as loadUrl,w as openPersonaJourney,h as screenQuestions,g as startPersonaLoad,y as takePendingLoad,T as walkNotice,C as walkPersona};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file A persona's route: the screens its responses take it through, worked out over the real
|
|
3
|
+
* flow graph and the workspace's own rules without touching the live session. Jump to step lists
|
|
4
|
+
* them, and the panel's builder draws them.
|
|
5
|
+
*/
|
|
6
|
+
import type { FieldValue, Picked } from '@picked/client';
|
|
7
|
+
import { evaluateRuleLogic } from '@picked/client';
|
|
8
|
+
import { type BuilderWalk, type Persona, type PersonaLanding } from '../../derive';
|
|
9
|
+
/** The rule type the client evaluates. */
|
|
10
|
+
export type Rule = Parameters<typeof evaluateRuleLogic>[1];
|
|
11
|
+
/** The parts of a page's address a rule on `location` reads. */
|
|
12
|
+
export interface EntryLocation {
|
|
13
|
+
hash: string;
|
|
14
|
+
host: string;
|
|
15
|
+
hostname: string;
|
|
16
|
+
href: string;
|
|
17
|
+
origin: string;
|
|
18
|
+
pathname: string;
|
|
19
|
+
port: string;
|
|
20
|
+
protocol: string;
|
|
21
|
+
search: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The entry page's address, as a rule on `location` reads it.
|
|
25
|
+
*
|
|
26
|
+
* @param {string} entryUrl - The entry page: a path on this site, with its query.
|
|
27
|
+
* @param {string} origin - This site's origin.
|
|
28
|
+
* @returns {EntryLocation} The address.
|
|
29
|
+
* @example entryLocation('/sale?utm=spring', 'https://shop.example').pathname // '/sale'
|
|
30
|
+
*/
|
|
31
|
+
export declare function entryLocation(entryUrl: string, origin: string): EntryLocation;
|
|
32
|
+
/**
|
|
33
|
+
* Evaluates rules against draft responses, on an entry page.
|
|
34
|
+
*
|
|
35
|
+
* @param {Picked} client - The client.
|
|
36
|
+
* @param {EntryLocation} location - The entry page's address.
|
|
37
|
+
* @returns {(rule: Rule, responses: Readonly<Record<string, FieldValue>>) => Promise<boolean>} The evaluator.
|
|
38
|
+
* @example await ruleEvaluator(client, location)(condition.logic, { purpose: 'home' })
|
|
39
|
+
*/
|
|
40
|
+
export declare function ruleEvaluator(client: Picked, location: EntryLocation): (rule: Rule, responses: Readonly<Record<string, FieldValue>>) => Promise<boolean>;
|
|
41
|
+
/** What a route reads beyond the persona. */
|
|
42
|
+
export interface RouteSettings {
|
|
43
|
+
/** Every key a journey's screens ask. */
|
|
44
|
+
asked: ReadonlySet<string>;
|
|
45
|
+
codeOwned: ReadonlySet<string>;
|
|
46
|
+
selfAdvancing: ReadonlySet<string>;
|
|
47
|
+
/** Actions that open another journey, by action key. */
|
|
48
|
+
handoffs: Readonly<Record<string, string>>;
|
|
49
|
+
/** Experience keys never walked. */
|
|
50
|
+
hiddenKeys: ReadonlySet<string>;
|
|
51
|
+
/** Responses every walk gives when a step asks for them, over the persona's own. */
|
|
52
|
+
responses: Readonly<Record<string, FieldValue>>;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The route a persona's responses take through the experiences on this page.
|
|
56
|
+
*
|
|
57
|
+
* @param {Picked} client - The client.
|
|
58
|
+
* @param {Persona} persona - The persona.
|
|
59
|
+
* @param {RouteSettings} settings - What the walk reads beyond the persona.
|
|
60
|
+
* @param {{ origin: string; pathname: string; search: string }} here - This page's address, for a persona with no entry page.
|
|
61
|
+
* @param {string} here.origin - Its origin.
|
|
62
|
+
* @param {string} here.pathname - Its path.
|
|
63
|
+
* @param {string} here.search - Its query.
|
|
64
|
+
* @returns {Promise<BuilderWalk | null>} The walk, or `null` for a persona with no journey.
|
|
65
|
+
* @example await personaRoute(client, persona, settings, window.location)
|
|
66
|
+
*/
|
|
67
|
+
export declare function personaRoute(client: Picked, persona: Persona, settings: RouteSettings, here: {
|
|
68
|
+
origin: string;
|
|
69
|
+
pathname: string;
|
|
70
|
+
search: string;
|
|
71
|
+
}): Promise<BuilderWalk | null>;
|
|
72
|
+
/** A screen on a persona's route. */
|
|
73
|
+
export interface RouteScreen extends PersonaLanding {
|
|
74
|
+
stepKey: string;
|
|
75
|
+
/** `true` on the screen the persona stops on. */
|
|
76
|
+
isStop: boolean;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The screens on a route, in order, marking the persona's own stop: the screen it names, else
|
|
80
|
+
* where its responses run out.
|
|
81
|
+
*
|
|
82
|
+
* @param {BuilderWalk} walk - The route.
|
|
83
|
+
* @param {PersonaLanding | null} landing - Where the persona stops.
|
|
84
|
+
* @returns {RouteScreen[]} The screens.
|
|
85
|
+
* @example routeScreens(walk, persona.landing)
|
|
86
|
+
*/
|
|
87
|
+
export declare function routeScreens(walk: BuilderWalk, landing: PersonaLanding | null): RouteScreen[];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{startOf as e}from"../../derive/personas/personas.js";import{builderGraph as t,responseKeysOf as n,walkDraft as r}from"../../derive/persona-builder/persona-builder.js";import"../../derive/index.js";import{evaluateRuleLogic as i}from"@picked/client";function a(e,t){let n=new URL(e,t);return{hash:n.hash,host:n.host,hostname:n.hostname,href:n.href,origin:n.origin,pathname:n.pathname,port:n.port,protocol:n.protocol,search:n.search}}function o(e,t){return async(n,r)=>!!await i(e,n,{responses:{...r},location:{...t}})}async function s(i,s,c,l){let u=e(s);if(u===null)return null;let d=new Map(i.fields.listStore.getState().map(e=>[e.key,e])),f=a(s.entryUrl??`${l.pathname}${l.search}`,l.origin);return r({...t(i.experiences.listStore.getState(),c.hiddenKeys),start:u,answers:{...s.responses,...c.responses},choices:s.choices,asked:c.asked,codeOwned:c.codeOwned,selfAdvancing:c.selfAdvancing,handoffs:c.handoffs,field:e=>d.get(e),evaluate:o(i,f),readsOf:n})}function c(e,t){let n=t?.stepKey==null?e.landing:t;return e.rows.flatMap(e=>e.kind===`screen`?[{experienceKey:e.experienceKey,experienceName:e.experienceName,stepKey:e.stepKey,stepName:e.stepName,isStop:n?.experienceKey===e.experienceKey&&n.stepKey===e.stepKey}]:[])}export{a as entryLocation,s as personaRoute,c as routeScreens,o as ruleEvaluator};
|