@picked/svelte 0.0.1-dev.1774231221701 → 0.0.1-dev.1774912386475
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/form/create-form.js +1 -1
- package/dist/form/create-form.svelte.d.ts +2 -2
- package/dist/hooks/field.d.ts +3 -2
- package/dist/hooks/field.js +1 -1
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/step.d.ts +13 -2
- package/dist/hooks/step.js +1 -1
- package/dist/plugins/devtools/components/ExperienceInstanceListItem.svelte +14 -4
- package/dist/plugins/devtools/components/ExperienceInstancesTable.svelte +1 -1
- package/dist/plugins/devtools/plugin.d.ts +9 -0
- package/dist/plugins/devtools/plugin.js +1 -1
- package/dist/plugins/devtools/screens/events/EventListItem.svelte +121 -37
- package/dist/plugins/devtools/screens/experiences/ExperienceDetailsScreen.svelte +2 -1
- package/dist/plugins/devtools/screens/experiences/ExperienceTreeScreen.svelte +100 -14
- package/dist/plugins/devtools/screens/experiences/ExperiencesListScreen.svelte +2 -1
- package/dist/plugins/devtools/screens/history/HistoryEntryListItem.svelte +7 -1
- package/dist/plugins/devtools/screens/responses/ResponseTableRow.svelte +81 -13
- package/dist/plugins/devtools/screens/responses/ResponsesTable.svelte +13 -4
- package/dist/router/Step.svelte +11 -4
- package/dist/router/StepRouter.svelte +3 -2
- package/dist/router/context.d.ts +1 -0
- package/package.json +4 -4
package/dist/form/create-form.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{getExperience as e,getPicked as t}from"../hooks/client.js";import{createForm as n}from"@picked/client/form";import*as r from"svelte/internal/client";function i(i,a,o){let s=n({client:t(),experienceInstance:e()},i,a,o);return{...s,form:{...s.form,
|
|
1
|
+
import{getExperience as e,getPicked as t}from"../hooks/client.js";import{createForm as n}from"@picked/client/form";import*as r from"svelte/internal/client";function i(i,a,o){let s=n({client:t(),experienceInstance:e()},i,a,o);return{...s,form:{...s.form,onBeforeSubmit:e=>{r.user_effect(()=>s.form.onBeforeSubmit(e))},onSuccess:e=>{r.user_effect(()=>s.form.onSuccess(e))},onError:e=>{r.user_effect(()=>s.form.onError(e))}}}}export{i as createForm};
|
|
@@ -6,8 +6,8 @@ export type { FormField } from '@picked/client/form';
|
|
|
6
6
|
* (cleanup is handled automatically via $effect, not by the consumer).
|
|
7
7
|
*/
|
|
8
8
|
export interface FormInstance<TFieldIds extends string = string> extends Omit<CoreFormInstance<TFieldIds>, 'form'> {
|
|
9
|
-
form: Omit<CoreFormInstance<TFieldIds>['form'], '
|
|
10
|
-
|
|
9
|
+
form: Omit<CoreFormInstance<TFieldIds>['form'], 'onBeforeSubmit' | 'onSuccess' | 'onError'> & {
|
|
10
|
+
onBeforeSubmit: (callback: SubmitCallback<TFieldIds>) => void;
|
|
11
11
|
onSuccess: (callback: SuccessCallback<TFieldIds>) => void;
|
|
12
12
|
onError: (callback: ErrorCallback) => void;
|
|
13
13
|
};
|
package/dist/hooks/field.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Field, FieldValue } from '@picked/client';
|
|
2
2
|
import { Store } from 'effector';
|
|
3
|
-
|
|
3
|
+
interface GetFieldResponse {
|
|
4
4
|
value: Store<FieldValue>;
|
|
5
5
|
field: Store<Field | null>;
|
|
6
6
|
}
|
|
7
|
-
export declare function getField(
|
|
7
|
+
export declare function getField(key: string): GetFieldResponse;
|
|
8
|
+
export {};
|
package/dist/hooks/field.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{getPicked as e}from"./client.js";function t(t){let n=e();return{field:n.fields.
|
|
1
|
+
import{getPicked as e}from"./client.js";function t(t){let n=e();return{field:n.fields.getByKey(t),value:n.responses.getByFieldKey(t)}}export{t as getField};
|
package/dist/hooks/index.d.ts
CHANGED
package/dist/hooks/step.d.ts
CHANGED
|
@@ -3,11 +3,22 @@ import { ExperienceStep } from '@picked/client';
|
|
|
3
3
|
import { FormInstance, FormField } from '../form/create-form.svelte';
|
|
4
4
|
export interface StepContext extends FormInstance {
|
|
5
5
|
step: Store<ExperienceStep>;
|
|
6
|
-
/** Directly accessible fields with current response values — usable without a form */
|
|
6
|
+
/** Directly accessible fields with current response values — usable without a form. */
|
|
7
7
|
fields: Store<FormField[]>;
|
|
8
8
|
isActive: Store<boolean>;
|
|
9
|
-
/** @deprecated Use `isActive` instead */
|
|
9
|
+
/** @deprecated Use `isActive` instead. */
|
|
10
10
|
isActiveStep: Store<boolean>;
|
|
11
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* Creates and sets step context in the Svelte context tree.
|
|
14
|
+
*
|
|
15
|
+
* @param stepId - The step identifier.
|
|
16
|
+
* @example setStepContext('step-1')
|
|
17
|
+
*/
|
|
12
18
|
export declare function setStepContext(stepId: string): StepContext;
|
|
19
|
+
/**
|
|
20
|
+
* Retrieves the step context from the Svelte context tree.
|
|
21
|
+
*
|
|
22
|
+
* @example getStep()
|
|
23
|
+
*/
|
|
13
24
|
export declare function getStep(): StepContext;
|
package/dist/hooks/step.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{STEP_CONTEXT_KEY as e}from"../context.js";import{
|
|
1
|
+
import{STEP_CONTEXT_KEY as e}from"../context.js";import{getPicked as t}from"./client.js";import{getStepRouter as n}from"../router/context.js";import{getStepFields as r}from"@picked/client/form";import{attach as i,combine as a,createEffect as o,createStore as s,sample as c}from"effector";import{getContext as l,setContext as u}from"svelte";import{createForm as d}from"../form/create-form.svelte";function f(l){let f=t(),{activeStep:p}=n(),m=p.map(e=>e?.key===l),h=f.steps.listStore.map(e=>e.find(e=>e.key===l)??{id:l,key:l,name:``,fieldIds:[]}),g=s([]),_=i({source:a({step:h,fields:f.fields.listStore}),mapParams:(e,t)=>t,effect:o(async({step:e,fields:t})=>await r(f,t.filter(t=>e.fieldIds.includes(t.id)||e.fieldIds.includes(t.key))))});c({source:{isActiveStep:m},filter:({isActiveStep:e})=>e,target:_}),c({clock:f.responses.listStore,source:{isActiveStep:m},filter:({isActiveStep:e})=>e,target:_}),c({source:_.doneData,target:g});let v=g.map(e=>e.map(e=>e.id));return u(e,{step:h,isActive:m,isActiveStep:m,...d(l,h.map(e=>e.name),v)})}function p(){return l(e)}export{p as getStep,f as setStepContext};
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
/** @file List item row displaying an experience instance's current state in the devtools panel. */
|
|
2
3
|
import type { ExperienceInstance } from '@picked/client'
|
|
3
4
|
import { Button } from '../ui'
|
|
5
|
+
import { getPicked } from '../../../hooks'
|
|
4
6
|
|
|
5
7
|
export let experienceInstance: ExperienceInstance
|
|
6
8
|
|
|
9
|
+
const client = getPicked()
|
|
10
|
+
const experienceStore = client.experiences.get(experienceInstance.experienceId)
|
|
11
|
+
|
|
7
12
|
const currentNode = experienceInstance.currentNodeStore
|
|
8
13
|
const currentStep = experienceInstance.currentStepStore
|
|
9
14
|
const nextStepNodes = experienceInstance.nextReachableStepNodesStore
|
|
@@ -12,15 +17,20 @@
|
|
|
12
17
|
const isOpen = experienceInstance.isOpenStore
|
|
13
18
|
|
|
14
19
|
$: totalNextStepNodes = $nextStepNodes.length
|
|
20
|
+
$: experienceName = $experienceStore?.name ?? experienceInstance.experienceId
|
|
21
|
+
$: currentNodeDisplay = $currentNode
|
|
22
|
+
? (('stepKey' in $currentNode && $currentNode.stepKey) || $currentNode.id)
|
|
23
|
+
: ''
|
|
24
|
+
$: currentStepDisplay = $currentStep?.name ?? ''
|
|
15
25
|
</script>
|
|
16
26
|
|
|
17
27
|
<tr>
|
|
18
|
-
<td>{
|
|
28
|
+
<td>{experienceName}</td>
|
|
19
29
|
<td><Button variant="ghost" size="small" onclick={() => experienceInstance.toggleOpen()}>{$isOpen}</Button></td>
|
|
20
|
-
<td>{
|
|
30
|
+
<td>{currentNodeDisplay}</td>
|
|
21
31
|
<td>{$currentNode?.type || ''}</td>
|
|
22
|
-
<td>{
|
|
23
|
-
<td>{$currentStep?.
|
|
32
|
+
<td>{currentStepDisplay}</td>
|
|
33
|
+
<td>{$currentStep?.name || ''}</td>
|
|
24
34
|
<td>{totalNextStepNodes}</td>
|
|
25
35
|
<td>{$stepsToOrigin[0]} - {$stepsToOrigin[1]}</td>
|
|
26
36
|
<td>{$stepsToLeaf[0]} - {$stepsToLeaf[1]}</td>
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
export let experienceInstances: ExperienceInstance[]
|
|
7
7
|
|
|
8
8
|
const columns = [
|
|
9
|
-
{ key: 'experience', header: '
|
|
9
|
+
{ key: 'experience', header: 'Name' },
|
|
10
10
|
{ key: 'isOpen', header: 'Is Open' },
|
|
11
11
|
{ key: 'currentNodeId', header: 'Current node ID' },
|
|
12
12
|
{ key: 'currentNodeType', header: 'Current node Type' },
|
|
@@ -11,5 +11,14 @@ export interface SvelteDevtoolsPluginOptions {
|
|
|
11
11
|
/**
|
|
12
12
|
* Registers the devtools experience for on-demand loading via the Chrome DevTools extension.
|
|
13
13
|
* The devtools component is lazily loaded — it is never included in the initial bundle.
|
|
14
|
+
*
|
|
15
|
+
* @param options - Optional configuration for the devtools plugin.
|
|
16
|
+
* @returns An array of plugins to register with the Picked client.
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* const client = new Picked({
|
|
20
|
+
* plugins: [...svelteDevtoolsPlugin({ enabled: true })],
|
|
21
|
+
* })
|
|
22
|
+
* ```
|
|
14
23
|
*/
|
|
15
24
|
export declare const svelteDevtoolsPlugin: (options?: SvelteDevtoolsPluginOptions) => Plugin[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{adaptSvelteComponent as e}from"../../component.js";import{__vitePreload as t}from"../../_virtual/_vite/preload-helper.js";import{lazyComponent as n}from"@picked/client";import{createExperience as r,createExperiencePlugin as i}from"@picked/client/plugins/create-experience";var a=a=>{let o=r({id:`devtools`,name:`Picked Devtools`,condition:!1,initializeOpen:!1,targets:[{selector:`body`,placement:`append`}],component:e(n(()=>t(()=>import(`./DevToolsExperience.svelte`),[])))});return o.addStep({
|
|
1
|
+
import{adaptSvelteComponent as e}from"../../component.js";import{__vitePreload as t}from"../../_virtual/_vite/preload-helper.js";import{lazyComponent as n}from"@picked/client";import{createExperience as r,createExperiencePlugin as i}from"@picked/client/plugins/create-experience";var a=a=>{let o=r({id:`devtools`,key:`devtools`,name:`Picked Devtools`,condition:!1,initializeOpen:!1,targets:[{selector:`body`,placement:`append`}],component:e(n(()=>t(()=>import(`./DevToolsExperience.svelte`),[])))});return o.addStep({name:`Tools`}),[{name:`svelte-devtools`,setup:e=>{a?.enabled&&e.eventBus.watch(t=>{t.type===`ready`&&e.experiences.launch({key:o.key,options:{bypassCondition:!0}})})}},i({experiences:[o]})]};export{a as svelteDevtoolsPlugin};
|
|
@@ -1,11 +1,33 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
/** @file Expandable list item row for displaying an event bus history entry in the devtools events screen. */
|
|
2
3
|
import type { EventBusPayload, EventHistoryEntry } from '@picked/client'
|
|
3
4
|
import { Badge, Card, CodeBlock, Stack, Text } from '../../ui'
|
|
5
|
+
import { getPicked } from '../../../../hooks'
|
|
4
6
|
|
|
5
7
|
export let event: EventHistoryEntry
|
|
6
8
|
export let isExpanded: boolean
|
|
7
9
|
export let onToggle: () => void
|
|
8
10
|
|
|
11
|
+
const client = getPicked()
|
|
12
|
+
|
|
13
|
+
const eventExpId =
|
|
14
|
+
'experience' in event.payload && event.payload.experience
|
|
15
|
+
? event.payload.experience.experienceId
|
|
16
|
+
: undefined
|
|
17
|
+
const eventExperienceStore = client.experiences.get(eventExpId ?? '')
|
|
18
|
+
$: eventExpName = eventExpId
|
|
19
|
+
? ($eventExperienceStore?.name ?? eventExpId)
|
|
20
|
+
: '-'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Formats a Date as a 24-hour time string with milliseconds.
|
|
24
|
+
*
|
|
25
|
+
* @param date - The date to format.
|
|
26
|
+
* @example
|
|
27
|
+
* ```typescript
|
|
28
|
+
* formatTime(new Date()) // '14:30:05.123'
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
9
31
|
function formatTime(date: Date): string {
|
|
10
32
|
return date.toLocaleTimeString('en-US', {
|
|
11
33
|
hour12: false,
|
|
@@ -21,53 +43,107 @@
|
|
|
21
43
|
value: unknown
|
|
22
44
|
}
|
|
23
45
|
|
|
24
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Extracts experience-related fields from an event payload's experience object.
|
|
48
|
+
*
|
|
49
|
+
* @param exp - The experience object from the event payload.
|
|
50
|
+
* @example
|
|
51
|
+
* ```typescript
|
|
52
|
+
* getExperienceFields({ id: 'abc', experienceId: 'quiz' })
|
|
53
|
+
* // [{ label: 'Experience ID', value: 'abc' }, { label: 'Experience Type', value: 'quiz' }]
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
function getExperienceFields(exp: unknown): PayloadField[] {
|
|
57
|
+
if (!exp || typeof exp !== 'object') return []
|
|
25
58
|
const fields: PayloadField[] = []
|
|
59
|
+
if ('id' in exp && exp.id) fields.push({ label: 'Experience ID', value: exp.id })
|
|
60
|
+
if ('experienceId' in exp && exp.experienceId)
|
|
61
|
+
fields.push({ label: 'Experience Type', value: exp.experienceId })
|
|
62
|
+
return fields
|
|
63
|
+
}
|
|
26
64
|
|
|
27
|
-
|
|
65
|
+
/**
|
|
66
|
+
* Extracts step-related fields from an event payload's step object.
|
|
67
|
+
*
|
|
68
|
+
* @param step - The step object from the event payload.
|
|
69
|
+
* @example
|
|
70
|
+
* ```typescript
|
|
71
|
+
* getStepFields({ id: 's1', name: 'Welcome' })
|
|
72
|
+
* // [{ label: 'Step ID', value: 's1' }, { label: 'Step Name', value: 'Welcome' }]
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
function getStepFields(step: unknown): PayloadField[] {
|
|
76
|
+
if (!step || typeof step !== 'object'){
|
|
77
|
+
return []
|
|
78
|
+
}
|
|
28
79
|
|
|
29
|
-
|
|
30
|
-
|
|
80
|
+
const fields: PayloadField[] = []
|
|
81
|
+
if ('key' in step && step.key) {
|
|
82
|
+
fields.push({ label: 'Step Key', value: step.key })
|
|
31
83
|
}
|
|
32
84
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
fields.push({ label: 'Experience ID', value: exp.id })
|
|
38
|
-
}
|
|
39
|
-
if ('experienceId' in exp && exp.experienceId) {
|
|
40
|
-
fields.push({ label: 'Experience Type', value: exp.experienceId })
|
|
41
|
-
}
|
|
42
|
-
}
|
|
85
|
+
// eslint-disable-next-line sonarjs/deprecation -- backwards-compat fallback for label
|
|
86
|
+
const stepName = ('name' in step && step.name) || ('label' in step && step.label)
|
|
87
|
+
if (stepName) {
|
|
88
|
+
fields.push({ label: 'Step Name', value: stepName })
|
|
43
89
|
}
|
|
44
90
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
91
|
+
return fields
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Scalar payload keys to extract and their display labels. JSON-serializable values are stringified. */
|
|
95
|
+
const SCALAR_KEYS: { key: string; label: string; serialize?: boolean }[] = [
|
|
96
|
+
{ key: 'value', label: 'Value' },
|
|
97
|
+
{ key: 'form', label: 'Form', serialize: true },
|
|
98
|
+
{ key: 'feature', label: 'Feature', serialize: true },
|
|
99
|
+
{ key: 'exception', label: 'Exception', serialize: true },
|
|
100
|
+
{ key: 'version', label: 'Version' },
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Extracts scalar (non-nested) fields from an event bus payload.
|
|
105
|
+
*
|
|
106
|
+
* @param payload - The event bus payload to extract scalar fields from.
|
|
107
|
+
* @example
|
|
108
|
+
* ```typescript
|
|
109
|
+
* getScalarFields({ type: 'step', value: 42 })
|
|
110
|
+
* // [{ label: 'Value', value: 42 }]
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
function getScalarFields(payload: EventBusPayload): PayloadField[] {
|
|
114
|
+
const fields: PayloadField[] = []
|
|
115
|
+
const record = payload as Record<string, unknown>
|
|
116
|
+
|
|
117
|
+
for (const { key, label, serialize } of SCALAR_KEYS) {
|
|
118
|
+
const raw = record[key]
|
|
119
|
+
if (raw !== undefined) {
|
|
120
|
+
fields.push({ label, value: serialize ? JSON.stringify(raw) : raw })
|
|
54
121
|
}
|
|
55
122
|
}
|
|
56
123
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
124
|
+
return fields
|
|
125
|
+
}
|
|
60
126
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
127
|
+
/**
|
|
128
|
+
* Builds a flat list of display fields from an event bus payload.
|
|
129
|
+
*
|
|
130
|
+
* @param payload - The event bus payload to extract fields from.
|
|
131
|
+
* @example
|
|
132
|
+
* ```typescript
|
|
133
|
+
* getPayloadFields({ type: 'session', id: 'sess-1' })
|
|
134
|
+
* // [{ label: 'Type', value: 'session' }, { label: 'Session ID', value: 'sess-1' }]
|
|
135
|
+
* ```
|
|
136
|
+
*/
|
|
137
|
+
function getPayloadFields(payload: EventBusPayload): PayloadField[] {
|
|
138
|
+
const fields: PayloadField[] = [{ label: 'Type', value: payload.type }]
|
|
139
|
+
fields.push(...getScalarFields(payload))
|
|
64
140
|
|
|
65
|
-
if ('
|
|
66
|
-
fields.push(
|
|
141
|
+
if ('experience' in payload && payload.experience) {
|
|
142
|
+
fields.push(...getExperienceFields(payload.experience))
|
|
67
143
|
}
|
|
68
144
|
|
|
69
|
-
if ('
|
|
70
|
-
fields.push(
|
|
145
|
+
if ('step' in payload && payload.step) {
|
|
146
|
+
fields.push(...getStepFields(payload.step))
|
|
71
147
|
}
|
|
72
148
|
|
|
73
149
|
if ('id' in payload && payload.id && payload.type === 'session') {
|
|
@@ -77,6 +153,16 @@
|
|
|
77
153
|
return fields
|
|
78
154
|
}
|
|
79
155
|
|
|
156
|
+
/**
|
|
157
|
+
* Formats a field value for display as a string.
|
|
158
|
+
*
|
|
159
|
+
* @param value - The value to format.
|
|
160
|
+
* @example
|
|
161
|
+
* ```typescript
|
|
162
|
+
* formatValue(42) // '42'
|
|
163
|
+
* formatValue(null) // 'null'
|
|
164
|
+
* ```
|
|
165
|
+
*/
|
|
80
166
|
function formatValue(value: unknown): string {
|
|
81
167
|
if (value === null) return 'null'
|
|
82
168
|
if (value === undefined) return 'undefined'
|
|
@@ -97,9 +183,7 @@
|
|
|
97
183
|
<Badge status="info">{event.payload.type}</Badge>
|
|
98
184
|
</td>
|
|
99
185
|
<td>
|
|
100
|
-
<Text size="small">
|
|
101
|
-
{'experience' in event.payload && event.payload.experience ? event.payload.experience.experienceId : '-'}
|
|
102
|
-
</Text>
|
|
186
|
+
<Text size="small">{eventExpName}</Text>
|
|
103
187
|
</td>
|
|
104
188
|
<td>
|
|
105
189
|
<Text size="caption" color="subdued">
|
|
@@ -114,7 +198,7 @@
|
|
|
114
198
|
<Text size="small" weight="semibold">Event Payload</Text>
|
|
115
199
|
<Card>
|
|
116
200
|
<Stack direction="vertical" gap="small">
|
|
117
|
-
{#each getPayloadFields(event.payload) as field}
|
|
201
|
+
{#each getPayloadFields(event.payload) as field (field.label)}
|
|
118
202
|
<Text size="caption" inline>
|
|
119
203
|
<Text size="caption" weight="semibold" color="subdued" inline>{field.label}:</Text>
|
|
120
204
|
<CodeBlock code={formatValue(field.value)} inline />
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
/** @file Detail screen for a single experience, showing its config, target selectors, and decision logic. */
|
|
2
3
|
import type { ExperienceStore } from '@picked/client'
|
|
3
4
|
import { getPicked } from '../../../../hooks'
|
|
4
5
|
import { Button, Card, Text, CodeBlock, Stack } from '../../ui'
|
|
@@ -11,7 +12,7 @@
|
|
|
11
12
|
|
|
12
13
|
<Stack direction="vertical" gap="medium" padding="medium">
|
|
13
14
|
<Stack direction="horizontal" gap="small">
|
|
14
|
-
<Button variant="primary" size="small" onclick={() => client.
|
|
15
|
+
<Button variant="primary" size="small" onclick={() => client.experiences.open({ key: experience.key, options: { bypassCondition: true } })}>
|
|
15
16
|
Launch experience
|
|
16
17
|
</Button>
|
|
17
18
|
</Stack>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
/** @file D3-based tree visualization of an experience's node graph in the devtools panel. */
|
|
2
3
|
import * as d3 from 'd3'
|
|
3
4
|
import type { ExperienceEdge, ExperienceNode, ExperienceStore } from '@picked/client'
|
|
4
5
|
import { Badge, Button, Spinner, Stack, Text, VisualizationContainer } from '../../ui'
|
|
@@ -7,7 +8,9 @@
|
|
|
7
8
|
|
|
8
9
|
let svgEl: SVGSVGElement
|
|
9
10
|
let zoom = 1
|
|
11
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- reserved for future node details panel
|
|
10
12
|
let selectedNode = null
|
|
13
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars, sonarjs/no-unused-collection -- reserved for future path highlighting
|
|
11
14
|
let highlightedPath: string[][] = []
|
|
12
15
|
let error: string | null = null
|
|
13
16
|
const loading = false
|
|
@@ -16,10 +19,9 @@
|
|
|
16
19
|
|
|
17
20
|
const tree = experience.treeStore
|
|
18
21
|
|
|
19
|
-
$: steps = $tree.steps
|
|
20
22
|
$: nodes = $tree.nodes
|
|
21
23
|
$: edges = $tree.edges
|
|
22
|
-
$:
|
|
24
|
+
$: conditions = $tree.conditions
|
|
23
25
|
|
|
24
26
|
let zoomBehavior: d3.ZoomBehavior<SVGSVGElement, unknown>
|
|
25
27
|
|
|
@@ -35,10 +37,30 @@
|
|
|
35
37
|
_referenceTo?: string
|
|
36
38
|
}
|
|
37
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Type guard that checks if a value is a non-null, non-array object.
|
|
42
|
+
*
|
|
43
|
+
* @param val - The value to check.
|
|
44
|
+
* @example
|
|
45
|
+
* ```typescript
|
|
46
|
+
* isRecord({ key: 'value' }) // true
|
|
47
|
+
* isRecord([1, 2]) // false
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
38
50
|
function isRecord(val: unknown): val is Record<string, unknown> {
|
|
39
51
|
return val !== null && typeof val === 'object' && !Array.isArray(val)
|
|
40
52
|
}
|
|
41
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Recursively formats a JSON Logic rule into a human-readable string.
|
|
56
|
+
*
|
|
57
|
+
* @param rule - The JSON Logic rule object to format.
|
|
58
|
+
* @example
|
|
59
|
+
* ```typescript
|
|
60
|
+
* formatLogicRule({ '==': [{ var: 'age' }, 18] }) // 'age = 18'
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
// eslint-disable-next-line sonarjs/cognitive-complexity -- recursive logic rule formatter handles all operator types
|
|
42
64
|
function formatLogicRule(rule: unknown): string {
|
|
43
65
|
if (rule === null || rule === undefined) return '?'
|
|
44
66
|
if (typeof rule === 'string') return rule
|
|
@@ -46,6 +68,7 @@
|
|
|
46
68
|
if (Array.isArray(rule)) {
|
|
47
69
|
return rule.map(formatLogicRule).join(', ')
|
|
48
70
|
}
|
|
71
|
+
|
|
49
72
|
if (isRecord(rule)) {
|
|
50
73
|
const keys = Object.keys(rule)
|
|
51
74
|
if (keys.length === 0) return 'Condition'
|
|
@@ -69,28 +92,43 @@
|
|
|
69
92
|
if (op in compOps && Array.isArray(args) && args.length >= 2) {
|
|
70
93
|
return `${formatLogicRule(args[0])} ${compOps[op]} ${formatLogicRule(args[1])}`
|
|
71
94
|
}
|
|
95
|
+
|
|
72
96
|
if (op === 'and' && Array.isArray(args)) {
|
|
73
97
|
return args.map(formatLogicRule).join(' & ')
|
|
74
98
|
}
|
|
99
|
+
|
|
75
100
|
if (op === 'or' && Array.isArray(args)) {
|
|
76
101
|
return args.map(formatLogicRule).join(' | ')
|
|
77
102
|
}
|
|
103
|
+
|
|
78
104
|
if ((op === '!' || op === 'not') && Array.isArray(args)) {
|
|
79
105
|
return `!${formatLogicRule(args[0])}`
|
|
80
106
|
}
|
|
107
|
+
|
|
81
108
|
if (op === 'in' && Array.isArray(args) && args.length >= 2) {
|
|
82
109
|
return `${formatLogicRule(args[0])} in ${formatLogicRule(args[1])}`
|
|
83
110
|
}
|
|
111
|
+
|
|
84
112
|
return `${op}(?)`
|
|
85
113
|
}
|
|
114
|
+
|
|
86
115
|
return '?'
|
|
87
116
|
}
|
|
88
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Renders the experience node graph as an interactive D3 tree visualization.
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* ```typescript
|
|
123
|
+
* renderGraph() // builds and renders the SVG tree
|
|
124
|
+
* ```
|
|
125
|
+
*/
|
|
89
126
|
function renderGraph() {
|
|
90
127
|
d3.select(svgEl).selectAll('*').remove()
|
|
91
128
|
|
|
92
|
-
|
|
93
|
-
const
|
|
129
|
+
// eslint-disable-next-line svelte/prefer-svelte-reactivity -- D3 visualization runs imperatively outside Svelte reactivity
|
|
130
|
+
const conditionsMap = new Map(conditions.map((c) => [c.id, c]))
|
|
131
|
+
// eslint-disable-next-line svelte/prefer-svelte-reactivity -- D3 visualization runs imperatively outside Svelte reactivity
|
|
94
132
|
const outgoingEdges = new Map<string, ExperienceEdge[]>()
|
|
95
133
|
edges.forEach((e) => {
|
|
96
134
|
if (!outgoingEdges.has(e.sourceNodeId)) outgoingEdges.set(e.sourceNodeId, [])
|
|
@@ -99,17 +137,20 @@
|
|
|
99
137
|
|
|
100
138
|
const getNodeLabel = (node: ExperienceNode): string => {
|
|
101
139
|
if (node.type === 'origin') return 'Start'
|
|
102
|
-
if (node.type === 'step'
|
|
103
|
-
return
|
|
104
|
-
if (node.type === '
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
if (
|
|
140
|
+
if (node.type === 'step')
|
|
141
|
+
return node.name ?? 'Unknown Step'
|
|
142
|
+
if (node.type === 'condition') {
|
|
143
|
+
const conditionId = node.conditionId
|
|
144
|
+
const conditionItem = conditionId ? conditionsMap.get(conditionId) : undefined
|
|
145
|
+
if (conditionItem?.name) return conditionItem.name
|
|
146
|
+
if (conditionItem?.logic) return formatLogicRule(conditionItem.logic)
|
|
108
147
|
return 'Condition'
|
|
109
148
|
}
|
|
149
|
+
|
|
110
150
|
return 'Unknown Node'
|
|
111
151
|
}
|
|
112
152
|
|
|
153
|
+
// eslint-disable-next-line svelte/prefer-svelte-reactivity -- D3 visualization runs imperatively outside Svelte reactivity
|
|
113
154
|
const processedNodes = new Map<string, ProcessedNode>()
|
|
114
155
|
nodes.forEach((n) => {
|
|
115
156
|
processedNodes.set(n.id, {
|
|
@@ -141,6 +182,7 @@
|
|
|
141
182
|
// Use a global visited set so join nodes (multiple incoming branches converging
|
|
142
183
|
// to the same node) only appear once in the tree, with reference nodes on
|
|
143
184
|
// subsequent visits to indicate the convergence point.
|
|
185
|
+
// eslint-disable-next-line svelte/prefer-svelte-reactivity -- D3 visualization runs imperatively outside Svelte reactivity
|
|
144
186
|
const globalVisited = new Set<string>()
|
|
145
187
|
const convertGraphToTree = (node: ProcessedNode): ProcessedNode => {
|
|
146
188
|
if (globalVisited.has(node.id)) {
|
|
@@ -196,8 +238,9 @@
|
|
|
196
238
|
.append('path')
|
|
197
239
|
.attr('class', 'link')
|
|
198
240
|
.attr('fill', 'none')
|
|
241
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- D3 callback types are untyped
|
|
199
242
|
.attr('stroke', (d: any) => {
|
|
200
|
-
if (d.source.data.type !== '
|
|
243
|
+
if (d.source.data.type !== 'condition') return '#94a3b8'
|
|
201
244
|
const targetId = d.target.data._isReference
|
|
202
245
|
? d.target.data._referenceTo
|
|
203
246
|
: d.target.data.id
|
|
@@ -211,17 +254,21 @@
|
|
|
211
254
|
.attr('stroke-width', 2)
|
|
212
255
|
.attr('d', linkGenerator)
|
|
213
256
|
|
|
214
|
-
// Label true/false branches from
|
|
257
|
+
// Label true/false branches from condition nodes
|
|
215
258
|
g.selectAll('.edge-label')
|
|
216
|
-
|
|
259
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- D3 callback types are untyped
|
|
260
|
+
.data(treeRoot.links().filter((d: any) => d.source.data.type === 'condition'))
|
|
217
261
|
.enter()
|
|
218
262
|
.append('text')
|
|
219
263
|
.attr('class', 'edge-label')
|
|
220
264
|
.attr('text-anchor', 'middle')
|
|
221
265
|
.attr('font-size', '10px')
|
|
222
266
|
.attr('font-weight', 'bold')
|
|
267
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- D3 callback types are untyped
|
|
223
268
|
.attr('x', (d: any) => (d.source.y + d.target.y) / 2)
|
|
269
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- D3 callback types are untyped
|
|
224
270
|
.attr('y', (d: any) => (d.source.x + d.target.x) / 2 - 6)
|
|
271
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- D3 callback types are untyped
|
|
225
272
|
.attr('fill', (d: any) => {
|
|
226
273
|
const targetId = d.target.data._isReference
|
|
227
274
|
? d.target.data._referenceTo
|
|
@@ -233,6 +280,7 @@
|
|
|
233
280
|
if (edge?.sourceNodeHandle === 'false') return '#ef4444'
|
|
234
281
|
return '#64748b'
|
|
235
282
|
})
|
|
283
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- D3 callback types are untyped
|
|
236
284
|
.text((d: any) => {
|
|
237
285
|
const targetId = d.target.data._isReference
|
|
238
286
|
? d.target.data._referenceTo
|
|
@@ -251,15 +299,18 @@
|
|
|
251
299
|
.enter()
|
|
252
300
|
.append('g')
|
|
253
301
|
.attr('class', 'node')
|
|
302
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- D3 callback types are untyped
|
|
254
303
|
.attr('transform', (d: any) => `translate(${d.y},${d.x})`)
|
|
255
304
|
|
|
256
305
|
nodesSelection
|
|
257
306
|
.append('circle')
|
|
307
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- D3 callback types are untyped
|
|
258
308
|
.attr('r', (d: any) => (d.data.type === 'origin' ? 15 : 10))
|
|
309
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- D3 callback types are untyped
|
|
259
310
|
.attr('fill', (d: any) => {
|
|
260
311
|
if (d.data._isReference) return '#38bdf8'
|
|
261
312
|
if (d.data.type === 'origin') return '#00cd90'
|
|
262
|
-
if (d.data.type === '
|
|
313
|
+
if (d.data.type === 'condition') return '#f59e0b'
|
|
263
314
|
return '#1748ff'
|
|
264
315
|
})
|
|
265
316
|
.attr('stroke', '#fff')
|
|
@@ -267,10 +318,12 @@
|
|
|
267
318
|
|
|
268
319
|
nodesSelection
|
|
269
320
|
.append('text')
|
|
321
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- D3 callback types are untyped
|
|
270
322
|
.attr('dy', (d: any) => (d.children ? -15 : 25))
|
|
271
323
|
.attr('text-anchor', 'middle')
|
|
272
324
|
.attr('font-size', '12px')
|
|
273
325
|
.attr('fill', '#1e293b')
|
|
326
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- D3 callback types are untyped
|
|
274
327
|
.text((d: any) =>
|
|
275
328
|
d.data.name.length > 30 ? `${d.data.name.slice(0, 27)}...` : d.data.name,
|
|
276
329
|
)
|
|
@@ -301,11 +354,27 @@
|
|
|
301
354
|
treeRenderingDone = true
|
|
302
355
|
}
|
|
303
356
|
|
|
357
|
+
/**
|
|
358
|
+
* Zooms the tree visualization in by 20%.
|
|
359
|
+
*
|
|
360
|
+
* @example
|
|
361
|
+
* ```typescript
|
|
362
|
+
* handleZoomIn() // increases zoom level
|
|
363
|
+
* ```
|
|
364
|
+
*/
|
|
304
365
|
function handleZoomIn() {
|
|
305
366
|
if (!svgEl || !zoomBehavior) return
|
|
306
367
|
d3.select(svgEl).transition().duration(250).call(zoomBehavior.scaleBy, 1.2)
|
|
307
368
|
}
|
|
308
369
|
|
|
370
|
+
/**
|
|
371
|
+
* Zooms the tree visualization out by 20%.
|
|
372
|
+
*
|
|
373
|
+
* @example
|
|
374
|
+
* ```typescript
|
|
375
|
+
* handleZoomOut() // decreases zoom level
|
|
376
|
+
* ```
|
|
377
|
+
*/
|
|
309
378
|
function handleZoomOut() {
|
|
310
379
|
if (!svgEl || !zoomBehavior) return
|
|
311
380
|
d3.select(svgEl)
|
|
@@ -314,6 +383,14 @@
|
|
|
314
383
|
.call(zoomBehavior.scaleBy, 1 / 1.2)
|
|
315
384
|
}
|
|
316
385
|
|
|
386
|
+
/**
|
|
387
|
+
* Resets the tree visualization zoom and position to the default centered view.
|
|
388
|
+
*
|
|
389
|
+
* @example
|
|
390
|
+
* ```typescript
|
|
391
|
+
* handleReset() // resets zoom to default
|
|
392
|
+
* ```
|
|
393
|
+
*/
|
|
317
394
|
function handleReset() {
|
|
318
395
|
if (!svgEl || !zoomBehavior) return
|
|
319
396
|
const width = svgEl.clientWidth
|
|
@@ -327,6 +404,15 @@
|
|
|
327
404
|
d3.select(svgEl).transition().duration(750).call(zoomBehavior.transform, initialTransform)
|
|
328
405
|
}
|
|
329
406
|
|
|
407
|
+
/**
|
|
408
|
+
* Closes the node details panel and resets all link/node highlights.
|
|
409
|
+
*
|
|
410
|
+
* @example
|
|
411
|
+
* ```typescript
|
|
412
|
+
* closeNodeDetails() // resets selection and highlighting
|
|
413
|
+
* ```
|
|
414
|
+
*/
|
|
415
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- reserved for future node details panel
|
|
330
416
|
function closeNodeDetails() {
|
|
331
417
|
selectedNode = null
|
|
332
418
|
if (svgEl) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
/** @file Screen listing all registered experiences as tabs in the devtools panel. */
|
|
2
3
|
import Tabs from '../../components/Tabs.svelte'
|
|
3
4
|
import { getPicked } from '../../../../hooks'
|
|
4
5
|
import ExperienceScreen from './ExperienceScreen.svelte'
|
|
@@ -9,7 +10,7 @@
|
|
|
9
10
|
$: tabs = $experiences.map((experience) => {
|
|
10
11
|
return {
|
|
11
12
|
id: experience.id,
|
|
12
|
-
label: experience.
|
|
13
|
+
label: experience.name,
|
|
13
14
|
component: ExperienceScreen,
|
|
14
15
|
props: {
|
|
15
16
|
experience,
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
/** @file List item row displaying a navigation history entry in the devtools history screen. */
|
|
2
3
|
import type { HistoryEntry } from '@picked/client'
|
|
4
|
+
import { getPicked } from '../../../../hooks'
|
|
3
5
|
|
|
4
6
|
export let historyEntry: HistoryEntry
|
|
7
|
+
|
|
8
|
+
const client = getPicked()
|
|
9
|
+
const experienceStore = client.experiences.get(historyEntry.experienceId)
|
|
10
|
+
$: experienceName = $experienceStore?.name ?? historyEntry.experienceId
|
|
5
11
|
</script>
|
|
6
12
|
|
|
7
13
|
<tr>
|
|
8
14
|
<td>{historyEntry.id}</td>
|
|
9
|
-
<td>{
|
|
15
|
+
<td>{experienceName}</td>
|
|
10
16
|
<td>{historyEntry.node.id}</td>
|
|
11
17
|
<td>{historyEntry.node.type}</td>
|
|
12
18
|
<td>{historyEntry.at}</td>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
|
|
2
|
+
/** @file Editable table row for a single field response in the devtools responses screen. */
|
|
3
|
+
import type { ResponseListItem, PrimitiveFieldValue, Field } from '@picked/client'
|
|
3
4
|
import { getPicked } from '../../../../hooks'
|
|
4
5
|
import { Button, Text, CodeBlock, Banner, Stack } from '../../ui'
|
|
5
6
|
|
|
@@ -11,25 +12,47 @@
|
|
|
11
12
|
let isEditing = false
|
|
12
13
|
let editValue = ''
|
|
13
14
|
let editError = ''
|
|
14
|
-
|
|
15
|
+
// eslint-disable-next-line svelte/prefer-svelte-reactivity -- Set is reassigned for Svelte reactivity via `selectedOptions = selectedOptions`
|
|
16
|
+
let selectedOptions: Set<PrimitiveFieldValue> = new Set()
|
|
15
17
|
|
|
16
18
|
$: hasOptions = Array.isArray(field?.options) && field.options.length > 0
|
|
17
19
|
$: staticOptions = Array.isArray(field?.options) ? field.options : null
|
|
18
20
|
$: currentValue = response?.value ?? null
|
|
19
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Formats a response value for display, handling arrays, objects, and primitives.
|
|
24
|
+
*
|
|
25
|
+
* @param value - The response value to format.
|
|
26
|
+
* @example
|
|
27
|
+
* ```typescript
|
|
28
|
+
* formatValue(['a', 'b']) // 'a, b'
|
|
29
|
+
* formatValue(null) // '-'
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
20
32
|
function formatValue(value: unknown): string {
|
|
21
33
|
if (value === null || value === undefined) {
|
|
22
34
|
return '-'
|
|
23
35
|
}
|
|
36
|
+
|
|
24
37
|
if (Array.isArray(value) && value.every((item) => typeof item === 'string')) {
|
|
25
38
|
return value.join(', ')
|
|
26
39
|
}
|
|
40
|
+
|
|
27
41
|
if (typeof value === 'object') {
|
|
28
42
|
return JSON.stringify(value, null, 2)
|
|
29
43
|
}
|
|
44
|
+
|
|
30
45
|
return String(value)
|
|
31
46
|
}
|
|
32
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Enters edit mode for this field, initializing the edit state from the current value.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```typescript
|
|
53
|
+
* startEdit() // opens the inline editor
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
33
56
|
function startEdit() {
|
|
34
57
|
isEditing = true
|
|
35
58
|
editError = ''
|
|
@@ -46,6 +69,14 @@
|
|
|
46
69
|
}
|
|
47
70
|
}
|
|
48
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Cancels the current edit, discarding changes and resetting edit state.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```typescript
|
|
77
|
+
* cancelEdit() // closes editor without saving
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
49
80
|
function cancelEdit() {
|
|
50
81
|
isEditing = false
|
|
51
82
|
editValue = ''
|
|
@@ -53,51 +84,80 @@
|
|
|
53
84
|
selectedOptions = new Set()
|
|
54
85
|
}
|
|
55
86
|
|
|
56
|
-
|
|
87
|
+
/**
|
|
88
|
+
* Toggles selection of a field option, supporting both single and multi-select modes.
|
|
89
|
+
*
|
|
90
|
+
* @param optionValue - The option value to toggle.
|
|
91
|
+
* @example
|
|
92
|
+
* ```typescript
|
|
93
|
+
* toggleOption('red') // selects or deselects the 'red' option
|
|
94
|
+
* ```
|
|
95
|
+
*/
|
|
96
|
+
function toggleOption(optionValue: PrimitiveFieldValue) {
|
|
57
97
|
if (field?.multiple) {
|
|
58
98
|
if (selectedOptions.has(optionValue)) {
|
|
59
99
|
selectedOptions.delete(optionValue)
|
|
60
100
|
} else {
|
|
61
101
|
selectedOptions.add(optionValue)
|
|
62
102
|
}
|
|
103
|
+
|
|
63
104
|
selectedOptions = selectedOptions
|
|
64
105
|
} else {
|
|
65
106
|
selectedOptions = new Set([optionValue])
|
|
66
107
|
}
|
|
67
108
|
}
|
|
68
109
|
|
|
69
|
-
|
|
70
|
-
|
|
110
|
+
/**
|
|
111
|
+
* Parses a string input into a typed value based on the field's type.
|
|
112
|
+
*
|
|
113
|
+
* @param input - The raw string input to parse.
|
|
114
|
+
* @example
|
|
115
|
+
* ```typescript
|
|
116
|
+
* parseValue('42') // 42 (when field.type is 'number')
|
|
117
|
+
* parseValue('true') // true (when field.type is 'boolean')
|
|
118
|
+
* ```
|
|
119
|
+
*/
|
|
120
|
+
function parseValue(input: string): unknown {
|
|
121
|
+
const trimmed = input.trim()
|
|
71
122
|
|
|
72
|
-
if (
|
|
123
|
+
if (trimmed === '' || trimmed === '-') {
|
|
73
124
|
return null
|
|
74
125
|
}
|
|
75
126
|
|
|
76
|
-
if (
|
|
127
|
+
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
|
77
128
|
try {
|
|
78
|
-
return JSON.parse(
|
|
129
|
+
return JSON.parse(trimmed)
|
|
79
130
|
} catch {
|
|
80
131
|
throw new Error('Invalid JSON format')
|
|
81
132
|
}
|
|
82
133
|
}
|
|
83
134
|
|
|
84
135
|
if (field.type === 'boolean') {
|
|
85
|
-
if (
|
|
86
|
-
if (
|
|
136
|
+
if (trimmed === 'true') return true
|
|
137
|
+
if (trimmed === 'false') return false
|
|
87
138
|
throw new Error('Boolean must be "true" or "false"')
|
|
88
139
|
}
|
|
89
140
|
|
|
90
141
|
if (field.type === 'number') {
|
|
91
|
-
const num = Number(
|
|
142
|
+
const num = Number(trimmed)
|
|
92
143
|
if (isNaN(num)) {
|
|
93
144
|
throw new Error('Invalid number format')
|
|
94
145
|
}
|
|
146
|
+
|
|
95
147
|
return num
|
|
96
148
|
}
|
|
97
149
|
|
|
98
|
-
return
|
|
150
|
+
return trimmed
|
|
99
151
|
}
|
|
100
152
|
|
|
153
|
+
/**
|
|
154
|
+
* Commits the edited value via the client responses API and exits edit mode.
|
|
155
|
+
*
|
|
156
|
+
* @example
|
|
157
|
+
* ```typescript
|
|
158
|
+
* await saveEdit() // saves the current edit value
|
|
159
|
+
* ```
|
|
160
|
+
*/
|
|
101
161
|
async function saveEdit() {
|
|
102
162
|
try {
|
|
103
163
|
let newValue: FieldValue
|
|
@@ -128,6 +188,14 @@
|
|
|
128
188
|
}
|
|
129
189
|
}
|
|
130
190
|
|
|
191
|
+
/**
|
|
192
|
+
* Clears the response value for this field via the client responses API.
|
|
193
|
+
*
|
|
194
|
+
* @example
|
|
195
|
+
* ```typescript
|
|
196
|
+
* handleClear() // removes the response for this field
|
|
197
|
+
* ```
|
|
198
|
+
*/
|
|
131
199
|
function handleClear() {
|
|
132
200
|
client.responses.clear(field.id)
|
|
133
201
|
}
|
|
@@ -135,7 +203,7 @@
|
|
|
135
203
|
|
|
136
204
|
<tr>
|
|
137
205
|
<td>
|
|
138
|
-
<CodeBlock code={field.
|
|
206
|
+
<CodeBlock code={field.key} inline />
|
|
139
207
|
</td>
|
|
140
208
|
<td>
|
|
141
209
|
<Text size="small" weight="semibold">{field.label}</Text>
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
|
|
2
|
+
/** @file Table displaying all field responses with filtering and clear-all functionality in the devtools panel. */
|
|
3
|
+
import type { ResponseListItem } from '@picked/client'
|
|
3
4
|
import { getPicked } from '../../../../hooks'
|
|
4
|
-
import ResponseTableRow from './ResponseTableRow.svelte'
|
|
5
5
|
import { Card, DataTable, EmptyState, Button, RadioGroup, Stack } from '../../ui'
|
|
6
|
+
import ResponseTableRow from './ResponseTableRow.svelte'
|
|
6
7
|
|
|
7
8
|
export let responses: ResponseListItem[] = []
|
|
8
9
|
|
|
@@ -10,7 +11,7 @@
|
|
|
10
11
|
|
|
11
12
|
let filter = 'set'
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
const fieldsStore = client.fields.listStore
|
|
14
15
|
$: allFields = $fieldsStore ?? []
|
|
15
16
|
|
|
16
17
|
$: responseMap = new Map(responses.map((r) => [r.fieldId, r]))
|
|
@@ -29,12 +30,20 @@
|
|
|
29
30
|
return true
|
|
30
31
|
})
|
|
31
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Clears all committed field responses via the client responses API.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```typescript
|
|
38
|
+
* handleClearAll() // removes all responses
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
32
41
|
function handleClearAll() {
|
|
33
42
|
client.responses.clearAll()
|
|
34
43
|
}
|
|
35
44
|
|
|
36
45
|
const columns = [
|
|
37
|
-
{ key: '
|
|
46
|
+
{ key: 'key', header: 'Key' },
|
|
38
47
|
{ key: 'field', header: 'Field' },
|
|
39
48
|
{ key: 'value', header: 'Value' },
|
|
40
49
|
{ key: 'type', header: 'Type' },
|
package/dist/router/Step.svelte
CHANGED
|
@@ -1,23 +1,30 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
/** @file Step component that registers a step with the router and renders its content. */
|
|
2
3
|
import { type Component, type Snippet } from 'svelte'
|
|
3
4
|
import { setStepContext } from '../hooks'
|
|
4
5
|
import { getStepRouter } from './context'
|
|
5
6
|
|
|
6
7
|
interface Props {
|
|
7
|
-
|
|
8
|
+
/** Preferred prop: the step key used for registration and lookup. */
|
|
9
|
+
key?: string
|
|
10
|
+
/** @deprecated Use `key` instead. Accepted for backwards compatibility. */
|
|
11
|
+
id?: string
|
|
8
12
|
component?: Component | Promise<Component> | Promise<{ default: Component }>
|
|
9
13
|
children?: Snippet
|
|
10
14
|
}
|
|
11
15
|
|
|
12
|
-
const { id, component, children }: Props = $props()
|
|
16
|
+
const { key, id, component, children }: Props = $props()
|
|
17
|
+
|
|
18
|
+
const resolvedKey = key ?? id ?? ''
|
|
13
19
|
|
|
14
20
|
const step = {
|
|
15
|
-
id,
|
|
21
|
+
id: resolvedKey,
|
|
22
|
+
key: resolvedKey,
|
|
16
23
|
}
|
|
17
24
|
|
|
18
25
|
const { registerStep, unregisterStep } = getStepRouter()
|
|
19
26
|
|
|
20
|
-
setStepContext(
|
|
27
|
+
setStepContext(resolvedKey)
|
|
21
28
|
|
|
22
29
|
$effect(() => {
|
|
23
30
|
registerStep(step)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
/** @file Root router component that manages step registration and active step detection via Effector. */
|
|
2
3
|
import { createEvent, createStore, sample } from 'effector'
|
|
3
4
|
import { supportsWindow } from '@picked/client'
|
|
4
5
|
import { getExperience } from '../hooks'
|
|
@@ -34,7 +35,7 @@
|
|
|
34
35
|
// we just skip them.
|
|
35
36
|
if (hasActiveStep) return
|
|
36
37
|
|
|
37
|
-
const isMatchingStep = step.
|
|
38
|
+
const isMatchingStep = step.key === currentStepStore.getState()?.key
|
|
38
39
|
|
|
39
40
|
if (isMatchingStep) {
|
|
40
41
|
setActiveStepEvent(step)
|
|
@@ -51,7 +52,7 @@
|
|
|
51
52
|
|
|
52
53
|
sample({
|
|
53
54
|
source: { steps: stepsStore, currentStep: currentStepStore },
|
|
54
|
-
fn: ({ steps, currentStep }) => steps.find((s) => s.
|
|
55
|
+
fn: ({ steps, currentStep }) => steps.find((s) => s.key === currentStep?.key) || null,
|
|
55
56
|
target: setActiveStepEvent,
|
|
56
57
|
})
|
|
57
58
|
|
package/dist/router/context.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@picked/svelte",
|
|
3
|
-
"version": "0.0.1-dev.
|
|
3
|
+
"version": "0.0.1-dev.1774912386475",
|
|
4
4
|
"dependencies": {
|
|
5
5
|
"d3": "^7.9.0"
|
|
6
6
|
},
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"@vitest/coverage-v8": "^4.1.0",
|
|
19
19
|
"effector": "^23.3.0",
|
|
20
20
|
"fast-glob": "^3.3.3",
|
|
21
|
-
"happy-dom": "^20.
|
|
21
|
+
"happy-dom": "^20.8.8",
|
|
22
22
|
"size-limit": "^12.0.0",
|
|
23
23
|
"svelte": "^5.46.4",
|
|
24
24
|
"svelte-check": "^4.2.2",
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
},
|
|
89
89
|
"module": "./dist/index.js",
|
|
90
90
|
"peerDependencies": {
|
|
91
|
-
"@picked/client": "0.0.1-dev.
|
|
91
|
+
"@picked/client": "0.0.1-dev.1774912386475",
|
|
92
92
|
"effector": "^23.3.0",
|
|
93
93
|
"svelte": "^5.46.4"
|
|
94
94
|
},
|
|
@@ -108,7 +108,7 @@
|
|
|
108
108
|
"svelte-check": "svelte-check --tsconfig ./tsconfig.json",
|
|
109
109
|
"svelte-check:report": "mkdir -p ./.reports && pnpm svelte-check --tsconfig ./tsconfig.json --output=machine > ./.reports/svelte-check.txt || [ -f './.reports/svelte-check.txt' ]",
|
|
110
110
|
"test": "vitest run",
|
|
111
|
-
"test:coverage": "vitest
|
|
111
|
+
"test:coverage": "../../scripts/vitest-coverage.sh",
|
|
112
112
|
"test:packaged-types": "attw --pack",
|
|
113
113
|
"test:watch": "vitest",
|
|
114
114
|
"type-check": "tsgo -p . --noEmit"
|