@picked/svelte 0.0.51-next.1 → 0.0.51-next.11
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/hooks/action.d.ts +25 -2
- package/dist/hooks/action.js +1 -1
- 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 +14 -3
- 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/components/Tabs.svelte +22 -2
- 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 +127 -37
- package/dist/plugins/devtools/screens/experiences/ExperienceDetailsScreen.svelte +2 -5
- 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 +15 -6
- package/dist/router/Step.svelte +6 -4
- package/dist/router/StepRouter.svelte +3 -2
- package/dist/router/context.d.ts +1 -1
- package/dist/utils/ErrorBoundary.svelte +1 -1
- package/package.json +2 -2
package/dist/hooks/action.d.ts
CHANGED
|
@@ -1,2 +1,25 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { Action } from '@picked/client';
|
|
2
|
+
import { Store } from 'effector';
|
|
3
|
+
/**
|
|
4
|
+
* Returns the reactive evokable action store for the given action name.
|
|
5
|
+
*
|
|
6
|
+
* @param {string} actionName - The registered action name.
|
|
7
|
+
* @returns {Store<() => ReturnType<Action>>} A store containing the callable action function or undefined.
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* const actionStore = getAction('logEvent')
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
export declare function getAction(actionName: string): Store<() => ReturnType<Action>>;
|
|
14
|
+
/**
|
|
15
|
+
* Returns a reactive store holding the latest result of invoking the named action.
|
|
16
|
+
* Re-evaluates automatically when the underlying action store updates.
|
|
17
|
+
*
|
|
18
|
+
* @param {string} actionName - The registered action name.
|
|
19
|
+
* @returns {Store<string | string[] | boolean | void | undefined>} A store containing the latest action return value.
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* const result = getActionResult('isLoggedIn')
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export declare function getActionResult(actionName: string): import('effector').StoreWritable<string | boolean | void | string[] | undefined>;
|
package/dist/hooks/action.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{getPicked as e}from"./client.js";import{attach as t,createEffect as n,createEvent as r,createStore as i,sample as a}from"effector";function o(t){return e().actions.getEvokableAction(t)}function s(e){let s=o(e),c=i(void 0,{skipVoid:!1}),l=r(),u=t({source:s,mapParams:(e,t)=>t,effect:n(async e=>{if(e)return e()})});return a({clock:l,target:u}),a({clock:u.doneData,target:c}),l(),c}export{o as getAction,s as getActionResult};
|
|
1
|
+
import{getPicked as e}from"./client.js";import{attach as t,createEffect as n,createEvent as r,createStore as i,sample as a}from"effector";import"@picked/client";function o(t){return e().actions.getEvokableAction(t)}function s(e){let s=o(e),c=i(void 0,{skipVoid:!1}),l=r(),u=t({source:s,mapParams:(e,t)=>t,effect:n(async e=>{if(e)return await e()})});return a({clock:l,target:u}),a({clock:u.doneData,target:c}),l(),a({clock:s,target:l}),c}export{o as getAction,s as getActionResult};
|
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
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Creates and sets step context in the Svelte context tree.
|
|
14
|
+
*
|
|
15
|
+
* @param stepKey - The step key.
|
|
16
|
+
* @example setStepContext('step-1')
|
|
17
|
+
*/
|
|
18
|
+
export declare function setStepContext(stepKey: 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{getContext as r,setContext as i}from"svelte";import{createForm as a}from"../form/create-form.svelte";function o(r){let o=t(),{activeStep:s}=n(),c=s.map(e=>e?.key===r),l=o.steps.listStore.map(e=>e.find(e=>e.key===r)??{id:r,key:r,name:``,fieldIds:[]});return i(e,{step:l,isActive:c,isActiveStep:c,...a(r,l.map(e=>e.name),l.map(e=>e.fieldIds))})}function s(){return r(e)}export{s as getStep,o 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.getById(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' },
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
/** @file Accessible tab container that renders dynamic Svelte components per tab. */
|
|
2
3
|
import type { Component } from 'svelte'
|
|
3
4
|
|
|
4
5
|
interface Tab {
|
|
5
6
|
id: string
|
|
6
7
|
label: string
|
|
7
|
-
component: Component<
|
|
8
|
+
component: Component<Record<string, unknown>>
|
|
8
9
|
props?: Record<string, unknown>
|
|
9
10
|
}
|
|
10
11
|
|
|
@@ -13,10 +14,28 @@
|
|
|
13
14
|
|
|
14
15
|
let activeTab = initialTab
|
|
15
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Sets the active tab to the given index.
|
|
19
|
+
*
|
|
20
|
+
* @param {number} index - The zero-based tab index to activate.
|
|
21
|
+
* @example
|
|
22
|
+
* ```typescript
|
|
23
|
+
* activateTab(2) // switches to the third tab
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
16
26
|
function activateTab(index: number) {
|
|
17
27
|
activeTab = index
|
|
18
28
|
}
|
|
19
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Handles arrow-key, Home, and End navigation within the tab list.
|
|
32
|
+
*
|
|
33
|
+
* @param {KeyboardEvent} event - The keyboard event from the tab list.
|
|
34
|
+
* @example
|
|
35
|
+
* ```typescript
|
|
36
|
+
* handleKeydown(new KeyboardEvent('keydown', { key: 'ArrowRight' }))
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
20
39
|
function handleKeydown(event: KeyboardEvent) {
|
|
21
40
|
const tabCount = tabs.length
|
|
22
41
|
let newIndex = activeTab
|
|
@@ -83,6 +102,7 @@
|
|
|
83
102
|
display: flex;
|
|
84
103
|
flex-direction: column;
|
|
85
104
|
flex: 1;
|
|
105
|
+
min-height: 0;
|
|
86
106
|
}
|
|
87
107
|
|
|
88
108
|
.tab-list {
|
|
@@ -124,7 +144,7 @@
|
|
|
124
144
|
.tab-panel {
|
|
125
145
|
flex: 1 1 auto;
|
|
126
146
|
display: grid;
|
|
127
|
-
|
|
147
|
+
min-height: 0;
|
|
128
148
|
overflow: auto;
|
|
129
149
|
}
|
|
130
150
|
|
|
@@ -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,34 @@
|
|
|
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.getById(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} date - The date to format.
|
|
26
|
+
* @returns {string} The formatted time string.
|
|
27
|
+
* @example
|
|
28
|
+
* ```typescript
|
|
29
|
+
* formatTime(new Date()) // '14:30:05.123'
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
9
32
|
function formatTime(date: Date): string {
|
|
10
33
|
return date.toLocaleTimeString('en-US', {
|
|
11
34
|
hour12: false,
|
|
@@ -21,53 +44,111 @@
|
|
|
21
44
|
value: unknown
|
|
22
45
|
}
|
|
23
46
|
|
|
24
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Extracts experience-related fields from an event payload's experience object.
|
|
49
|
+
*
|
|
50
|
+
* @param {unknown} exp - The experience object from the event payload.
|
|
51
|
+
* @returns {PayloadField[]} Extracted experience fields.
|
|
52
|
+
* @example
|
|
53
|
+
* ```typescript
|
|
54
|
+
* getExperienceFields({ id: 'abc', experienceId: 'quiz' })
|
|
55
|
+
* // [{ label: 'Experience ID', value: 'abc' }, { label: 'Experience Type', value: 'quiz' }]
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
function getExperienceFields(exp: unknown): PayloadField[] {
|
|
59
|
+
if (!exp || typeof exp !== 'object') return []
|
|
25
60
|
const fields: PayloadField[] = []
|
|
61
|
+
if ('id' in exp && exp.id) fields.push({ label: 'Experience ID', value: exp.id })
|
|
62
|
+
if ('experienceId' in exp && exp.experienceId)
|
|
63
|
+
fields.push({ label: 'Experience Type', value: exp.experienceId })
|
|
64
|
+
return fields
|
|
65
|
+
}
|
|
26
66
|
|
|
27
|
-
|
|
67
|
+
/**
|
|
68
|
+
* Extracts step-related fields from an event payload's step object.
|
|
69
|
+
*
|
|
70
|
+
* @param {unknown} step - The step object from the event payload.
|
|
71
|
+
* @returns {PayloadField[]} Extracted step fields.
|
|
72
|
+
* @example
|
|
73
|
+
* ```typescript
|
|
74
|
+
* getStepFields({ id: 's1', name: 'Welcome' })
|
|
75
|
+
* // [{ label: 'Step ID', value: 's1' }, { label: 'Step Name', value: 'Welcome' }]
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
function getStepFields(step: unknown): PayloadField[] {
|
|
79
|
+
if (!step || typeof step !== 'object'){
|
|
80
|
+
return []
|
|
81
|
+
}
|
|
28
82
|
|
|
29
|
-
|
|
30
|
-
|
|
83
|
+
const fields: PayloadField[] = []
|
|
84
|
+
if ('key' in step && step.key) {
|
|
85
|
+
fields.push({ label: 'Step Key', value: step.key })
|
|
31
86
|
}
|
|
32
87
|
|
|
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
|
-
}
|
|
88
|
+
// eslint-disable-next-line sonarjs/deprecation -- backwards-compat fallback for label
|
|
89
|
+
const stepName = ('name' in step && step.name) || ('label' in step && step.label)
|
|
90
|
+
if (stepName) {
|
|
91
|
+
fields.push({ label: 'Step Name', value: stepName })
|
|
43
92
|
}
|
|
44
93
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
94
|
+
return fields
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Scalar payload keys to extract and their display labels. JSON-serializable values are stringified. */
|
|
98
|
+
const SCALAR_KEYS: { key: string; label: string; serialize?: boolean }[] = [
|
|
99
|
+
{ key: 'value', label: 'Value' },
|
|
100
|
+
{ key: 'form', label: 'Form', serialize: true },
|
|
101
|
+
{ key: 'feature', label: 'Feature', serialize: true },
|
|
102
|
+
{ key: 'exception', label: 'Exception', serialize: true },
|
|
103
|
+
{ key: 'version', label: 'Version' },
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Extracts scalar (non-nested) fields from an event bus payload.
|
|
108
|
+
*
|
|
109
|
+
* @param {EventBusPayload} payload - The event bus payload to extract scalar fields from.
|
|
110
|
+
* @returns {PayloadField[]} Extracted scalar fields.
|
|
111
|
+
* @example
|
|
112
|
+
* ```typescript
|
|
113
|
+
* getScalarFields({ type: 'step', value: 42 })
|
|
114
|
+
* // [{ label: 'Value', value: 42 }]
|
|
115
|
+
* ```
|
|
116
|
+
*/
|
|
117
|
+
function getScalarFields(payload: EventBusPayload): PayloadField[] {
|
|
118
|
+
const fields: PayloadField[] = []
|
|
119
|
+
const record = payload as Record<string, unknown>
|
|
120
|
+
|
|
121
|
+
for (const { key, label, serialize } of SCALAR_KEYS) {
|
|
122
|
+
const raw = record[key]
|
|
123
|
+
if (raw !== undefined) {
|
|
124
|
+
fields.push({ label, value: serialize ? JSON.stringify(raw) : raw })
|
|
54
125
|
}
|
|
55
126
|
}
|
|
56
127
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
128
|
+
return fields
|
|
129
|
+
}
|
|
60
130
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
131
|
+
/**
|
|
132
|
+
* Builds a flat list of display fields from an event bus payload.
|
|
133
|
+
*
|
|
134
|
+
* @param {EventBusPayload} payload - The event bus payload to extract fields from.
|
|
135
|
+
* @returns {PayloadField[]} All extracted fields.
|
|
136
|
+
* @example
|
|
137
|
+
* ```typescript
|
|
138
|
+
* getPayloadFields({ type: 'session', id: 'sess-1' })
|
|
139
|
+
* // [{ label: 'Type', value: 'session' }, { label: 'Session ID', value: 'sess-1' }]
|
|
140
|
+
* ```
|
|
141
|
+
*/
|
|
142
|
+
function getPayloadFields(payload: EventBusPayload): PayloadField[] {
|
|
143
|
+
const fields: PayloadField[] = [{ label: 'Type', value: payload.type }]
|
|
144
|
+
fields.push(...getScalarFields(payload))
|
|
64
145
|
|
|
65
|
-
if ('
|
|
66
|
-
fields.push(
|
|
146
|
+
if ('experience' in payload && payload.experience) {
|
|
147
|
+
fields.push(...getExperienceFields(payload.experience))
|
|
67
148
|
}
|
|
68
149
|
|
|
69
|
-
if ('
|
|
70
|
-
fields.push(
|
|
150
|
+
if ('step' in payload && payload.step) {
|
|
151
|
+
fields.push(...getStepFields(payload.step))
|
|
71
152
|
}
|
|
72
153
|
|
|
73
154
|
if ('id' in payload && payload.id && payload.type === 'session') {
|
|
@@ -77,6 +158,17 @@
|
|
|
77
158
|
return fields
|
|
78
159
|
}
|
|
79
160
|
|
|
161
|
+
/**
|
|
162
|
+
* Formats a field value for display as a string.
|
|
163
|
+
*
|
|
164
|
+
* @param {unknown} value - The value to format.
|
|
165
|
+
* @returns {string} The formatted string representation.
|
|
166
|
+
* @example
|
|
167
|
+
* ```typescript
|
|
168
|
+
* formatValue(42) // '42'
|
|
169
|
+
* formatValue(null) // 'null'
|
|
170
|
+
* ```
|
|
171
|
+
*/
|
|
80
172
|
function formatValue(value: unknown): string {
|
|
81
173
|
if (value === null) return 'null'
|
|
82
174
|
if (value === undefined) return 'undefined'
|
|
@@ -97,9 +189,7 @@
|
|
|
97
189
|
<Badge status="info">{event.payload.type}</Badge>
|
|
98
190
|
</td>
|
|
99
191
|
<td>
|
|
100
|
-
<Text size="small">
|
|
101
|
-
{'experience' in event.payload && event.payload.experience ? event.payload.experience.experienceId : '-'}
|
|
102
|
-
</Text>
|
|
192
|
+
<Text size="small">{eventExpName}</Text>
|
|
103
193
|
</td>
|
|
104
194
|
<td>
|
|
105
195
|
<Text size="caption" color="subdued">
|
|
@@ -114,7 +204,7 @@
|
|
|
114
204
|
<Text size="small" weight="semibold">Event Payload</Text>
|
|
115
205
|
<Card>
|
|
116
206
|
<Stack direction="vertical" gap="small">
|
|
117
|
-
{#each getPayloadFields(event.payload) as field}
|
|
207
|
+
{#each getPayloadFields(event.payload) as field (field.label)}
|
|
118
208
|
<Text size="caption" inline>
|
|
119
209
|
<Text size="caption" weight="semibold" color="subdued" inline>{field.label}:</Text>
|
|
120
210
|
<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,17 +12,13 @@
|
|
|
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>
|
|
18
19
|
|
|
19
20
|
<Card sectioned>
|
|
20
21
|
<Stack direction="vertical" gap="medium" padding="medium">
|
|
21
|
-
<div>
|
|
22
|
-
<Text weight="semibold">Type</Text>
|
|
23
|
-
<Text>{$config.type}</Text>
|
|
24
|
-
</div>
|
|
25
22
|
|
|
26
23
|
<div>
|
|
27
24
|
<Text weight="semibold">Target selectors</Text>
|
|
@@ -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,7 +19,6 @@
|
|
|
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
|
|
@@ -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
|
-
|
|
129
|
+
// eslint-disable-next-line svelte/prefer-svelte-reactivity -- D3 visualization runs imperatively outside Svelte reactivity
|
|
93
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 === 'condition'
|
|
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,6 +238,7 @@
|
|
|
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
243
|
if (d.source.data.type !== 'condition') return '#94a3b8'
|
|
201
244
|
const targetId = d.target.data._isReference
|
|
@@ -204,8 +247,8 @@
|
|
|
204
247
|
const edge = d.source.data._originalEdges.find(
|
|
205
248
|
(e: ExperienceEdge) => e.destinationNodeId === targetId,
|
|
206
249
|
)
|
|
207
|
-
if (edge?.
|
|
208
|
-
if (edge?.
|
|
250
|
+
if (edge?.sourceHandle === 'true') return '#10b981'
|
|
251
|
+
if (edge?.sourceHandle === 'false') return '#ef4444'
|
|
209
252
|
return '#94a3b8'
|
|
210
253
|
})
|
|
211
254
|
.attr('stroke-width', 2)
|
|
@@ -213,6 +256,7 @@
|
|
|
213
256
|
|
|
214
257
|
// Label true/false branches from condition nodes
|
|
215
258
|
g.selectAll('.edge-label')
|
|
259
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- D3 callback types are untyped
|
|
216
260
|
.data(treeRoot.links().filter((d: any) => d.source.data.type === 'condition'))
|
|
217
261
|
.enter()
|
|
218
262
|
.append('text')
|
|
@@ -220,8 +264,11 @@
|
|
|
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
|
|
@@ -229,10 +276,11 @@
|
|
|
229
276
|
const edge = d.source.data._originalEdges.find(
|
|
230
277
|
(e: ExperienceEdge) => e.destinationNodeId === targetId,
|
|
231
278
|
)
|
|
232
|
-
if (edge?.
|
|
233
|
-
if (edge?.
|
|
279
|
+
if (edge?.sourceHandle === 'true') return '#10b981'
|
|
280
|
+
if (edge?.sourceHandle === '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
|
|
@@ -240,8 +288,8 @@
|
|
|
240
288
|
const edge = d.source.data._originalEdges.find(
|
|
241
289
|
(e: ExperienceEdge) => e.destinationNodeId === targetId,
|
|
242
290
|
)
|
|
243
|
-
if (edge?.
|
|
244
|
-
if (edge?.
|
|
291
|
+
if (edge?.sourceHandle === 'true') return 'Yes'
|
|
292
|
+
if (edge?.sourceHandle === 'false') return 'No'
|
|
245
293
|
return ''
|
|
246
294
|
})
|
|
247
295
|
|
|
@@ -251,11 +299,14 @@
|
|
|
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'
|
|
@@ -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.getById(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,13 +11,13 @@
|
|
|
10
11
|
|
|
11
12
|
let filter = 'set'
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
const fieldsStore = client.fields.listStore
|
|
14
15
|
$: allFields = $fieldsStore ?? []
|
|
15
16
|
|
|
16
|
-
$: responseMap = new Map(responses.map((r) => [r.
|
|
17
|
+
$: responseMap = new Map(responses.map((r) => [r.fieldKey, r]))
|
|
17
18
|
|
|
18
19
|
$: allFieldRows = allFields.map((field) => {
|
|
19
|
-
const response = responseMap.get(field.
|
|
20
|
+
const response = responseMap.get(field.key)
|
|
20
21
|
return {
|
|
21
22
|
field,
|
|
22
23
|
response: response || null,
|
|
@@ -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,25 @@
|
|
|
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
|
+
key: string
|
|
8
9
|
component?: Component | Promise<Component> | Promise<{ default: Component }>
|
|
9
10
|
children?: Snippet
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
const {
|
|
13
|
+
const { key, component, children }: Props = $props()
|
|
14
|
+
|
|
13
15
|
|
|
14
16
|
const step = {
|
|
15
|
-
|
|
17
|
+
key,
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
const { registerStep, unregisterStep } = getStepRouter()
|
|
19
21
|
|
|
20
|
-
setStepContext(
|
|
22
|
+
setStepContext(key)
|
|
21
23
|
|
|
22
24
|
$effect(() => {
|
|
23
25
|
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
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
const { component: InnerComponent, componentProps }: Props = $props()
|
|
11
|
+
const client = getPicked()
|
|
11
12
|
|
|
12
13
|
function handleError(error: unknown) {
|
|
13
|
-
const client = getPicked()
|
|
14
14
|
|
|
15
15
|
// We still log to console for visibility during development
|
|
16
16
|
// eslint-disable-next-line no-console
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@picked/svelte",
|
|
3
|
-
"version": "0.0.51-next.
|
|
3
|
+
"version": "0.0.51-next.11",
|
|
4
4
|
"dependencies": {
|
|
5
5
|
"d3": "^7.9.0"
|
|
6
6
|
},
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
},
|
|
89
89
|
"module": "./dist/index.js",
|
|
90
90
|
"peerDependencies": {
|
|
91
|
-
"@picked/client": "0.0.51-next.
|
|
91
|
+
"@picked/client": "0.0.51-next.11",
|
|
92
92
|
"effector": "^23.3.0",
|
|
93
93
|
"svelte": "^5.46.4"
|
|
94
94
|
},
|