@picked/svelte 0.0.1-dev.1793772853474 → 0.0.1-dev.1793835766402
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/plugins/devtools/DevToolsExperience.svelte +169 -84
- package/dist/plugins/devtools/components/AdaptedToolHost.svelte +9 -3
- package/dist/plugins/devtools/customTools.d.ts +16 -7
- package/dist/plugins/devtools/customTools.js +1 -1
- package/dist/plugins/devtools/derive/answer-display/answer-display.d.ts +1 -1
- package/dist/plugins/devtools/derive/answer-display/answer-display.js +1 -1
- package/dist/plugins/devtools/derive/guide-sections/guide-sections.js +1 -1
- package/dist/plugins/devtools/derive/index.d.ts +3 -3
- package/dist/plugins/devtools/derive/index.js +1 -1
- package/dist/plugins/devtools/derive/journey-outline/journey-outline.js +1 -1
- package/dist/plugins/devtools/derive/journey-tree/journey-tree.js +1 -1
- package/dist/plugins/devtools/derive/persona-builder/persona-builder.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 +89 -8
- 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 +20 -25
- package/dist/plugins/devtools/derive/plain-language/plain-language.js +1 -1
- package/dist/plugins/devtools/derive/plain-logic/plain-logic.d.ts +1 -1
- package/dist/plugins/devtools/derive/plain-logic/plain-logic.js +1 -1
- package/dist/plugins/devtools/derive/safety-posture/safety-posture.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 +27 -24
- package/dist/plugins/devtools/options.js +1 -1
- package/dist/plugins/devtools/review/answers/AnswersPanel.svelte +14 -14
- package/dist/plugins/devtools/review/guide/GuidePanel.svelte +1 -1
- package/dist/plugins/devtools/review/index.d.ts +2 -2
- package/dist/plugins/devtools/review/journey/ExperienceDetails.svelte +129 -0
- package/dist/plugins/devtools/review/journey/JourneyMap.svelte +16 -14
- package/dist/plugins/devtools/review/journey/JourneyPanel.svelte +70 -24
- package/dist/plugins/devtools/review/journey/JourneyTimeline.svelte +22 -21
- package/dist/plugins/devtools/review/personas/PersonaBuilder.svelte +15 -46
- package/dist/plugins/devtools/review/personas/PersonaCard.svelte +227 -44
- package/dist/plugins/devtools/review/personas/PersonasPanel.svelte +191 -78
- package/dist/plugins/devtools/review/personas/load-persona.d.ts +43 -22
- 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/review/session/SessionPanel.svelte +11 -11
- package/dist/plugins/devtools/review/variations/VariationsPanel.svelte +12 -13
- package/dist/plugins/devtools/screens/custom-tools/CustomToolsScreen.svelte +11 -25
- package/dist/plugins/devtools/sidebar/Sidebar.svelte +2 -33
- package/dist/plugins/devtools/sidebar/SidebarRow.svelte +5 -42
- package/dist/plugins/devtools/sidebar/nav.d.ts +26 -28
- 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,21 +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
|
-
* the
|
|
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.
|
|
9
13
|
*/
|
|
10
14
|
import type { FieldValue, Picked } from '@picked/client'
|
|
11
15
|
import { getStepFields } from '@picked/client/form'
|
|
16
|
+
import { SvelteMap } from 'svelte/reactivity'
|
|
12
17
|
import { Banner, Button, Card, EmptyState, RadioGroup, Stack, Text, TextField } from '../../ui'
|
|
13
18
|
import {
|
|
19
|
+
applyEdits,
|
|
14
20
|
buildPersona,
|
|
15
21
|
customerAnswers,
|
|
16
22
|
deslug,
|
|
17
23
|
entryUrlOf,
|
|
24
|
+
groupPersonas,
|
|
18
25
|
isBlank,
|
|
26
|
+
matchesSearch,
|
|
19
27
|
passedScreens,
|
|
20
28
|
readSavedPersonas,
|
|
21
29
|
writeSavedPersonas,
|
|
@@ -27,8 +35,12 @@
|
|
|
27
35
|
type RouteEntry,
|
|
28
36
|
type SafetyPosture,
|
|
29
37
|
} from '../../derive'
|
|
38
|
+
import AdaptedToolHost from '../../components/AdaptedToolHost.svelte'
|
|
39
|
+
import type { CustomTool } from '../../customTools'
|
|
30
40
|
import type { JourneyView } from '../../session'
|
|
31
41
|
import { loadUrl, startPersonaLoad } from './load-persona'
|
|
42
|
+
import { personaLink } from './persona-session'
|
|
43
|
+
import { personaRoute, routeScreens, type RouteScreen } from './persona-route'
|
|
32
44
|
import PersonaBuilder from './PersonaBuilder.svelte'
|
|
33
45
|
import PersonaCard from './PersonaCard.svelte'
|
|
34
46
|
|
|
@@ -47,12 +59,22 @@
|
|
|
47
59
|
hiddenKeys: ReadonlySet<string>
|
|
48
60
|
/** Keys a screen carries only for its code. */
|
|
49
61
|
codeOwned: ReadonlySet<string>
|
|
50
|
-
/**
|
|
62
|
+
/**
|
|
63
|
+
* Screens that move on by themselves, responses every walk gives, and actions that open
|
|
64
|
+
* another journey.
|
|
65
|
+
*/
|
|
51
66
|
personaWalk: {
|
|
52
67
|
selfAdvancingSteps: ReadonlySet<string>
|
|
68
|
+
responses: Readonly<Record<string, FieldValue>>
|
|
53
69
|
handoffs: Readonly<Record<string, string>>
|
|
54
70
|
}
|
|
55
|
-
/**
|
|
71
|
+
/** The site's tools placed in this tab. */
|
|
72
|
+
tools?: readonly CustomTool[]
|
|
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. */
|
|
56
78
|
walkNotice: Notice | null
|
|
57
79
|
onWalkNoticeDismiss: () => void
|
|
58
80
|
/** The storage saved personas live in. */
|
|
@@ -83,6 +105,9 @@
|
|
|
83
105
|
hiddenKeys,
|
|
84
106
|
codeOwned,
|
|
85
107
|
personaWalk,
|
|
108
|
+
tools = [],
|
|
109
|
+
activePersona,
|
|
110
|
+
onStop,
|
|
86
111
|
walkNotice,
|
|
87
112
|
onWalkNoticeDismiss,
|
|
88
113
|
getStorage = () => window.localStorage,
|
|
@@ -123,26 +148,65 @@
|
|
|
123
148
|
let buildChoices = $state.raw<Record<string, boolean>>({})
|
|
124
149
|
let buildWalk = $state.raw<BuilderWalk | null>(null)
|
|
125
150
|
let notice = $state<Notice | null>(null)
|
|
126
|
-
let
|
|
151
|
+
let startingId = $state<string | null>(null)
|
|
152
|
+
let search = $state('')
|
|
127
153
|
/** The answers Save would keep; `null` while they are read. */
|
|
128
154
|
let captured = $state<Record<string, FieldValue> | null>(null)
|
|
129
155
|
|
|
130
|
-
const
|
|
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
|
+
)
|
|
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)
|
|
131
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>()
|
|
132
182
|
|
|
133
183
|
/**
|
|
134
|
-
* Why a persona
|
|
184
|
+
* Why starting a persona is withheld: the session's posture, or another persona starting.
|
|
135
185
|
*
|
|
136
186
|
* @param {Persona} persona - The persona on the card.
|
|
137
|
-
* @returns {string | undefined} The reason, or `undefined` while
|
|
138
|
-
* @example
|
|
187
|
+
* @returns {string | undefined} The reason, or `undefined` while it can start.
|
|
188
|
+
* @example startReason(persona)
|
|
139
189
|
*/
|
|
140
|
-
function
|
|
190
|
+
function startReason(persona: Persona): string | undefined {
|
|
141
191
|
if (postureReason !== undefined) return postureReason
|
|
142
|
-
return
|
|
143
|
-
? 'Another persona is
|
|
192
|
+
return startingId !== null && startingId !== persona.id
|
|
193
|
+
? 'Another persona is starting.'
|
|
144
194
|
: undefined
|
|
145
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
|
+
}
|
|
146
210
|
const landing = $derived<PersonaLanding | null>(
|
|
147
211
|
journey === null
|
|
148
212
|
? null
|
|
@@ -156,23 +220,23 @@
|
|
|
156
220
|
const buildSummary = $derived.by(() => {
|
|
157
221
|
if (buildWalk === null) return 'Working out the route…'
|
|
158
222
|
const count = Object.keys(buildWalk.answers).length
|
|
159
|
-
const answers = count === 1 ? '1
|
|
223
|
+
const answers = count === 1 ? '1 response' : `${count} responses`
|
|
160
224
|
const stop = buildWalk.landing
|
|
161
225
|
if (stop === null) return `Saves ${answers}.`
|
|
162
226
|
const screen = stop.stepName ?? deslug(stop.stepKey ?? '')
|
|
163
|
-
return `Saves ${answers}.
|
|
227
|
+
return `Saves ${answers}. It stops at ${screen}.`
|
|
164
228
|
})
|
|
165
229
|
|
|
166
230
|
const captureSummary = $derived.by(() => {
|
|
167
|
-
if (captured === null) return 'Reading your
|
|
231
|
+
if (captured === null) return 'Reading your responses…'
|
|
168
232
|
const count = Object.keys(captured).length
|
|
169
|
-
const answers = count === 1 ? '1
|
|
233
|
+
const answers = count === 1 ? '1 response' : `${count} responses`
|
|
170
234
|
if (landing === null)
|
|
171
|
-
return `Saves ${answers}. No
|
|
235
|
+
return `Saves ${answers}. No experience is open, so it keeps the responses only.`
|
|
172
236
|
const journeyName = landing.experienceName ?? deslug(landing.experienceKey)
|
|
173
|
-
if (landing.stepKey === null) return `Saves ${answers}.
|
|
237
|
+
if (landing.stepKey === null) return `Saves ${answers}. It starts in ${journeyName}.`
|
|
174
238
|
const screen = landing.stepName ?? deslug(landing.stepKey)
|
|
175
|
-
return `Saves ${answers}.
|
|
239
|
+
return `Saves ${answers}. It stops at ${screen} in ${journeyName}.`
|
|
176
240
|
})
|
|
177
241
|
|
|
178
242
|
/**
|
|
@@ -349,7 +413,7 @@
|
|
|
349
413
|
}
|
|
350
414
|
|
|
351
415
|
const edited = editingId !== null
|
|
352
|
-
saved =
|
|
416
|
+
saved = saved.some((entry) => entry.id === result.persona.id)
|
|
353
417
|
? saved.map((entry) => (entry.id === result.persona.id ? result.persona : entry))
|
|
354
418
|
: [...saved, result.persona]
|
|
355
419
|
creating = false
|
|
@@ -364,56 +428,69 @@
|
|
|
364
428
|
}
|
|
365
429
|
|
|
366
430
|
/**
|
|
367
|
-
* Deletes a saved persona.
|
|
431
|
+
* Deletes a saved persona, or drops this browser's edit of a site persona.
|
|
368
432
|
*
|
|
369
|
-
* @param {Persona} persona - The persona
|
|
433
|
+
* @param {Persona} persona - The persona.
|
|
370
434
|
* @example remove(persona)
|
|
371
435
|
*/
|
|
372
436
|
function remove(persona: Persona): void {
|
|
373
437
|
saved = saved.filter((entry) => entry.id !== persona.id)
|
|
374
438
|
writeSavedPersonas(getStorage, client.projectId, saved)
|
|
375
|
-
notice = {
|
|
439
|
+
notice = {
|
|
440
|
+
status: 'success',
|
|
441
|
+
text: persona.edited ? `Put ${persona.name} back as the site has it.` : `Deleted ${persona.name}.`,
|
|
442
|
+
}
|
|
376
443
|
}
|
|
377
444
|
|
|
378
445
|
/**
|
|
379
|
-
*
|
|
380
|
-
*
|
|
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.
|
|
381
448
|
*
|
|
382
|
-
* @param {Persona} persona - The persona
|
|
383
|
-
* @
|
|
384
|
-
* @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)
|
|
385
452
|
*/
|
|
386
|
-
|
|
387
|
-
|
|
453
|
+
function start(persona: Persona, stop: RouteScreen | null): void {
|
|
454
|
+
startingId = persona.id
|
|
388
455
|
notice = null
|
|
389
456
|
onWalkNoticeDismiss()
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
notice = { status: 'error', text: `${persona.name} did not load.${detail}` }
|
|
411
|
-
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
|
|
412
477
|
}
|
|
413
478
|
}
|
|
414
479
|
</script>
|
|
415
480
|
|
|
416
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
|
+
|
|
417
494
|
{#if walkNotice}
|
|
418
495
|
<Banner
|
|
419
496
|
status={walkNotice.status}
|
|
@@ -433,13 +510,18 @@
|
|
|
433
510
|
<div class="form">
|
|
434
511
|
<Stack direction="vertical" gap="medium">
|
|
435
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}
|
|
436
518
|
<RadioGroup
|
|
437
519
|
label="Make it by"
|
|
438
520
|
name="persona-mode"
|
|
439
521
|
orientation="horizontal"
|
|
440
522
|
value={mode}
|
|
441
523
|
options={[
|
|
442
|
-
{ value: 'build', label: '
|
|
524
|
+
{ value: 'build', label: 'Filling in the fields here' },
|
|
443
525
|
{ value: 'capture', label: 'Using where I am now' },
|
|
444
526
|
]}
|
|
445
527
|
onchange={(value) => (mode = value === 'capture' ? 'capture' : 'build')}
|
|
@@ -506,45 +588,63 @@
|
|
|
506
588
|
<div class="empty">
|
|
507
589
|
<EmptyState
|
|
508
590
|
message="No personas or scenarios yet"
|
|
509
|
-
hint="Walk the
|
|
591
|
+
hint="Walk the experience the way a visitor would, then save it here to load it again in one click."
|
|
510
592
|
/>
|
|
511
593
|
<Button variant="primary" onclick={startCreate}>Create</Button>
|
|
512
594
|
</div>
|
|
513
595
|
{:else}
|
|
514
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}
|
|
515
608
|
<Button variant="secondary" size="small" onclick={startCreate}>Create</Button>
|
|
516
609
|
</div>
|
|
517
610
|
{/if}
|
|
518
611
|
|
|
519
|
-
{#
|
|
520
|
-
<section class="group" aria-label=
|
|
521
|
-
<h3 class="group-title">
|
|
522
|
-
{#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)}
|
|
523
616
|
<PersonaCard
|
|
524
617
|
{persona}
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
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)}
|
|
624
|
+
onEdit={() => startEdit(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}
|
|
528
630
|
/>
|
|
529
631
|
{/each}
|
|
530
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>
|
|
531
637
|
{/if}
|
|
532
638
|
|
|
533
|
-
{#
|
|
534
|
-
<section class="group" aria-label=
|
|
535
|
-
<h3 class="group-title">
|
|
536
|
-
{#
|
|
537
|
-
<
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
loading={loadingId === persona.id}
|
|
541
|
-
onLoad={() => void load(persona)}
|
|
542
|
-
onEdit={() => startEdit(persona)}
|
|
543
|
-
onDelete={() => remove(persona)}
|
|
544
|
-
/>
|
|
545
|
-
{/each}
|
|
639
|
+
{#each tools as tool (tool.id)}
|
|
640
|
+
<section class="group" aria-label={tool.label} data-testid="devtools-persona-tool-{tool.id}">
|
|
641
|
+
<h3 class="group-title">{tool.label}</h3>
|
|
642
|
+
{#if tool.description}
|
|
643
|
+
<Text size="small" color="subdued">{tool.description}</Text>
|
|
644
|
+
{/if}
|
|
645
|
+
<AdaptedToolHost {tool} flush />
|
|
546
646
|
</section>
|
|
547
|
-
{/
|
|
647
|
+
{/each}
|
|
548
648
|
</div>
|
|
549
649
|
|
|
550
650
|
<style>
|
|
@@ -572,9 +672,22 @@
|
|
|
572
672
|
}
|
|
573
673
|
|
|
574
674
|
.toolbar {
|
|
675
|
+
align-items: flex-end;
|
|
575
676
|
justify-content: flex-end;
|
|
576
677
|
}
|
|
577
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
|
+
|
|
578
691
|
.group {
|
|
579
692
|
display: flex;
|
|
580
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;
|
|
@@ -83,8 +83,8 @@ export interface WalkSettings {
|
|
|
83
83
|
openOrder: () => readonly string[];
|
|
84
84
|
/** Step keys that move on by themselves, such as a loader. The walk waits on them. */
|
|
85
85
|
selfAdvancing: ReadonlySet<string>;
|
|
86
|
-
/**
|
|
87
|
-
|
|
86
|
+
/** Responses every walk gives when a step asks for their fields, over the persona's own. */
|
|
87
|
+
responses: Readonly<Record<string, FieldValue>>;
|
|
88
88
|
/**
|
|
89
89
|
* The keys a screen asks: its fields that show now, minus the code-owned ones.
|
|
90
90
|
*
|
|
@@ -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};
|