@picked/svelte 0.0.1-dev.1774231221701 → 0.0.1-dev.1774244905386
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/index.d.ts +25 -2
- package/dist/form/index.js +1 -1
- package/dist/hooks/field.d.ts +3 -2
- package/dist/hooks/field.js +1 -1
- package/dist/hooks/step.d.ts +8 -8
- package/dist/hooks/step.js +1 -1
- package/dist/plugins/devtools/components/ExperienceInstanceListItem.svelte +16 -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 +107 -47
- package/dist/plugins/devtools/screens/experiences/ExperienceDetailsScreen.svelte +2 -1
- package/dist/plugins/devtools/screens/experiences/ExperienceTreeScreen.svelte +101 -15
- 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 +86 -19
- package/dist/plugins/devtools/screens/responses/ResponsesTable.svelte +13 -4
- package/dist/router/Step.svelte +1 -0
- package/dist/router/StepRouter.svelte +2 -1
- package/dist/utils/get-step-fields.d.ts +2 -0
- package/dist/utils/get-step-fields.js +1 -0
- package/package.json +2 -2
- package/dist/form/create-form.js +0 -1
- package/dist/form/create-form.svelte.d.ts +0 -24
- package/dist/form/create-form.svelte.js +0 -1
- package/dist/form/get-form.d.ts +0 -16
- package/dist/form/get-form.js +0 -1
package/dist/form/index.d.ts
CHANGED
|
@@ -1,2 +1,25 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { ValidationError } from '@picked/client';
|
|
2
|
+
interface GetFormOptions {
|
|
3
|
+
id: string;
|
|
4
|
+
title: string;
|
|
5
|
+
onSuccess?: () => void | Promise<void>;
|
|
6
|
+
onError?: (errors: ValidationError[]) => void | Promise<void>;
|
|
7
|
+
}
|
|
8
|
+
type SubmitEventWithCurrentTarget = SubmitEvent & {
|
|
9
|
+
currentTarget: EventTarget & HTMLFormElement;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
*
|
|
13
|
+
* @param root0
|
|
14
|
+
* @param root0.id
|
|
15
|
+
* @param root0.title
|
|
16
|
+
* @param root0.onError
|
|
17
|
+
* @param root0.onSuccess
|
|
18
|
+
* @example
|
|
19
|
+
*/
|
|
20
|
+
export declare function getForm({ id: formId, title, onError, onSuccess }: GetFormOptions): {
|
|
21
|
+
form: {
|
|
22
|
+
onsubmit: import('effector').EventCallable<SubmitEventWithCurrentTarget>;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
export {};
|
package/dist/form/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{getExperience as e,getPicked as t}from"../hooks/client.js";import{getStep as n}from"../hooks/step.js";import"../hooks/index.js";import{attach as r,createEffect as i,createEvent as a,sample as o,split as s}from"effector";function c({id:c,title:l,onError:u,onSuccess:d}){let f=t(),p=n(),m=e(),{currentStepStore:h}=m,g=a(),_=i(e=>f.responses.commit(e)),v=r({source:{step:h},effect:i(async({step:e})=>{f.eventBus({type:`form_submit_success`,form:{id:c,title:l},experience:m,step:e??void 0}),await d?.()})}),y=r({source:{step:h},mapParams:(e,t)=>({step:t.step,errors:e.errors}),effect:i(async({step:e,errors:t})=>{f.eventBus({type:`form_submit_error`,form:{id:c,title:l},exception:{reason:t},experience:m,step:e??void 0}),t.forEach(t=>{f.eventBus({type:`field_validation_error`,exception:{reason:[t]},experience:m,step:e??void 0})}),await u?.(t)})});return o({clock:g,source:{step:h},fn:({step:e})=>({formId:c,title:l,step:e}),target:i(({formId:e,title:t,step:n})=>{f.eventBus({type:`form_submit`,form:{id:e,title:t},experience:m,step:n??void 0})})}),o({clock:g,source:{fields:f.fields.listStore,stepFields:p.fields,step:h},fn:({stepFields:e},t)=>{t.preventDefault();let n=new FormData(t.currentTarget),r=Array.from(n.entries()),i={data:{},push(e){let t=e.questionId,n=this.data[t];n&&Array.isArray(n)&&Array.isArray(e.value)?Array.isArray(n)&&Array.isArray(e.value)&&(typeof n[0]==`string`?this.data[t]=[...n,...e.value].map(String):typeof n[0]==`number`?this.data[t]=[...n,...e.value].map(Number):typeof n[0]==`boolean`?this.data[t]=[...n,...e.value].map(e=>!!e):this.data[t]=e.value):this.data[t]=e.value}};return e.forEach(e=>{let t=r.filter(([t])=>t===e.key||t===e.id);t.length===0&&(t=[[e.id,``]]),t.forEach(([,t])=>{let n=e.id;if(t===``){i.push({questionId:n,value:null});return}switch(e.type){case`text`:i.push({questionId:n,value:e.multiple?[String(t)]:String(t)});break;case`number`:i.push({questionId:n,value:e.multiple?[Number(t)]:Number(t)});break;case`boolean`:i.push({questionId:n,value:e.multiple?[t===`true`]:t===`true`});break;case`object`:break}})}),i.data},target:_}),s({source:_.doneData,match:{hasErrors:({errors:e})=>e.length>0},cases:{hasErrors:y,__:v}}),{form:{onsubmit:g}}}export{c as getForm};
|
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/step.d.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { Store } from 'effector';
|
|
2
|
-
import { ExperienceStep } from '@picked/client';
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
import { ExperienceStep, Field, Response } from '@picked/client';
|
|
3
|
+
type FieldWithValue = Field & {
|
|
4
|
+
value: Response | undefined;
|
|
5
|
+
};
|
|
6
|
+
export interface StepContext {
|
|
5
7
|
step: Store<ExperienceStep>;
|
|
6
|
-
|
|
7
|
-
fields: Store<FormField[]>;
|
|
8
|
-
isActive: Store<boolean>;
|
|
9
|
-
/** @deprecated Use `isActive` instead */
|
|
8
|
+
fields: Store<FieldWithValue[]>;
|
|
10
9
|
isActiveStep: Store<boolean>;
|
|
11
10
|
}
|
|
12
|
-
export declare function setStepContext(stepId: string): StepContext;
|
|
13
11
|
export declare function getStep(): StepContext;
|
|
12
|
+
export declare function setStepContext(stepId: string): StepContext;
|
|
13
|
+
export {};
|
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{getStepFields as n}from"../utils/get-step-fields.js";import{getStepRouter as r}from"../router/context.js";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";function d(){return l(e)}function f(l){let d=t(),{activeStep:f}=r(),p=f.map(e=>e?.id===l),m=d.steps.listStore.map(e=>{let t=e.find(e=>(e.key??e.id)===l);if(!t)throw Error(`Step with id "${l}" not found in the step registry.`);return t}),h=s([]),g=a({fields:h,responses:d.responses.listStore},({fields:e,responses:t})=>e.map(e=>{let n=t.find(t=>t.fieldKey===e.key||t.fieldId===e.id)?.value;return{...e,value:n}})),_=i({source:a({step:m,fields:d.fields.listStore}),effect:o(async({step:e,fields:t})=>await n(d,e.fieldKeys&&e.fieldKeys.length>0?t.filter(t=>(e.fieldKeys??[]).includes(t.key)):t.filter(t=>(e.fieldIds??[]).includes(t.id))))});return c({source:{isActiveStep:p},filter:({isActiveStep:e})=>e,target:_}),c({clock:d.responses.listStore,source:{isActiveStep:p},filter:({isActiveStep:e})=>e,target:_}),c({source:_.doneData,target:h}),u(e,{step:m,fields:g,isActiveStep:p})}export{d 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 experienceLookup = 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,22 @@
|
|
|
12
17
|
const isOpen = experienceInstance.isOpenStore
|
|
13
18
|
|
|
14
19
|
$: totalNextStepNodes = $nextStepNodes.length
|
|
20
|
+
$: experienceName = $experienceLookup?.name ?? experienceInstance.experienceId
|
|
21
|
+
$: currentNodeDisplay = $currentNode
|
|
22
|
+
? (('name' in $currentNode && $currentNode.name) ||
|
|
23
|
+
('stepKey' in $currentNode && $currentNode.stepKey) ||
|
|
24
|
+
$currentNode.id)
|
|
25
|
+
: ''
|
|
26
|
+
$: currentStepDisplay = $currentStep?.name ?? ''
|
|
15
27
|
</script>
|
|
16
28
|
|
|
17
29
|
<tr>
|
|
18
|
-
<td>{
|
|
30
|
+
<td>{experienceName}</td>
|
|
19
31
|
<td><Button variant="ghost" size="small" onclick={() => experienceInstance.toggleOpen()}>{$isOpen}</Button></td>
|
|
20
|
-
<td>{
|
|
32
|
+
<td>{currentNodeDisplay}</td>
|
|
21
33
|
<td>{$currentNode?.type || ''}</td>
|
|
22
|
-
<td>{
|
|
23
|
-
<td>{$currentStep?.
|
|
34
|
+
<td>{currentStepDisplay}</td>
|
|
35
|
+
<td>{$currentStep?.title || ''}</td>
|
|
24
36
|
<td>{totalNextStepNodes}</td>
|
|
25
37
|
<td>{$stepsToOrigin[0]} - {$stepsToOrigin[1]}</td>
|
|
26
38
|
<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({label:`Tools`}),[{name:`svelte-devtools`,setup:e=>{a?.enabled&&e.eventBus.watch(t=>{t.type===`ready`&&e.experiences.launch({
|
|
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({label:`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 eventExpLookup = client.experiences.get(eventExpId ?? '')
|
|
18
|
+
$: eventExpName = eventExpId
|
|
19
|
+
? ($eventExpLookup?.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,62 +43,102 @@
|
|
|
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') return []
|
|
77
|
+
const fields: PayloadField[] = []
|
|
78
|
+
if ('id' in step && step.id) fields.push({ label: 'Step ID', value: step.id })
|
|
79
|
+
// eslint-disable-next-line sonarjs/deprecation -- backwards-compat fallback for label
|
|
80
|
+
const stepName = ('name' in step && step.name) || ('label' in step && step.label)
|
|
81
|
+
if (stepName) fields.push({ label: 'Step Name', value: stepName })
|
|
82
|
+
return fields
|
|
83
|
+
}
|
|
28
84
|
|
|
29
|
-
|
|
85
|
+
/**
|
|
86
|
+
* Extracts scalar (non-nested) fields from an event bus payload.
|
|
87
|
+
*
|
|
88
|
+
* @param payload - The event bus payload to extract fields from.
|
|
89
|
+
* @example
|
|
90
|
+
* ```typescript
|
|
91
|
+
* getScalarFields({ type: 'step', value: 42 })
|
|
92
|
+
* // [{ label: 'Value', value: 42 }]
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
function getScalarFields(payload: EventBusPayload): PayloadField[] {
|
|
96
|
+
const fields: PayloadField[] = []
|
|
97
|
+
if ('value' in payload && payload.value !== undefined)
|
|
30
98
|
fields.push({ label: 'Value', value: payload.value })
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
if ('experience' in payload && payload.experience) {
|
|
34
|
-
const exp = payload.experience
|
|
35
|
-
if (exp && typeof exp === 'object') {
|
|
36
|
-
if ('id' in exp && exp.id) {
|
|
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
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if ('step' in payload && payload.step) {
|
|
46
|
-
const step = payload.step
|
|
47
|
-
if (step && typeof step === 'object') {
|
|
48
|
-
if ('id' in step && step.id) {
|
|
49
|
-
fields.push({ label: 'Step ID', value: step.id })
|
|
50
|
-
}
|
|
51
|
-
if ('label' in step && step.label) {
|
|
52
|
-
fields.push({ label: 'Step Label', value: step.label })
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if ('form' in payload && payload.form) {
|
|
99
|
+
if ('form' in payload && payload.form)
|
|
58
100
|
fields.push({ label: 'Form', value: JSON.stringify(payload.form) })
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if ('feature' in payload && payload.feature) {
|
|
101
|
+
if ('feature' in payload && payload.feature)
|
|
62
102
|
fields.push({ label: 'Feature', value: JSON.stringify(payload.feature) })
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if ('exception' in payload && payload.exception) {
|
|
103
|
+
if ('exception' in payload && payload.exception)
|
|
66
104
|
fields.push({ label: 'Exception', value: JSON.stringify(payload.exception) })
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if ('version' in payload && payload.version) {
|
|
105
|
+
if ('version' in payload && payload.version)
|
|
70
106
|
fields.push({ label: 'Version', value: payload.version })
|
|
71
|
-
|
|
107
|
+
return fields
|
|
108
|
+
}
|
|
72
109
|
|
|
73
|
-
|
|
110
|
+
/**
|
|
111
|
+
* Builds a complete list of display fields from an event bus payload.
|
|
112
|
+
*
|
|
113
|
+
* @param payload - The event bus payload to extract all fields from.
|
|
114
|
+
* @example
|
|
115
|
+
* ```typescript
|
|
116
|
+
* getPayloadFields({ type: 'session', id: 'sess-1' })
|
|
117
|
+
* // [{ label: 'Type', value: 'session' }, { label: 'Session ID', value: 'sess-1' }]
|
|
118
|
+
* ```
|
|
119
|
+
*/
|
|
120
|
+
function getPayloadFields(payload: EventBusPayload): PayloadField[] {
|
|
121
|
+
const fields: PayloadField[] = [{ label: 'Type', value: payload.type }]
|
|
122
|
+
fields.push(...getScalarFields(payload))
|
|
123
|
+
if ('experience' in payload && payload.experience)
|
|
124
|
+
fields.push(...getExperienceFields(payload.experience))
|
|
125
|
+
if ('step' in payload && payload.step)
|
|
126
|
+
fields.push(...getStepFields(payload.step))
|
|
127
|
+
if ('id' in payload && payload.id && payload.type === 'session')
|
|
74
128
|
fields.push({ label: 'Session ID', value: payload.id })
|
|
75
|
-
}
|
|
76
|
-
|
|
77
129
|
return fields
|
|
78
130
|
}
|
|
79
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Formats a field value for display as a string.
|
|
134
|
+
*
|
|
135
|
+
* @param value - The value to format.
|
|
136
|
+
* @example
|
|
137
|
+
* ```typescript
|
|
138
|
+
* formatValue(42) // '42'
|
|
139
|
+
* formatValue(null) // 'null'
|
|
140
|
+
* ```
|
|
141
|
+
*/
|
|
80
142
|
function formatValue(value: unknown): string {
|
|
81
143
|
if (value === null) return 'null'
|
|
82
144
|
if (value === undefined) return 'undefined'
|
|
@@ -97,9 +159,7 @@
|
|
|
97
159
|
<Badge status="info">{event.payload.type}</Badge>
|
|
98
160
|
</td>
|
|
99
161
|
<td>
|
|
100
|
-
<Text size="small">
|
|
101
|
-
{'experience' in event.payload && event.payload.experience ? event.payload.experience.experienceId : '-'}
|
|
102
|
-
</Text>
|
|
162
|
+
<Text size="small">{eventExpName}</Text>
|
|
103
163
|
</td>
|
|
104
164
|
<td>
|
|
105
165
|
<Text size="caption" color="subdued">
|
|
@@ -114,7 +174,7 @@
|
|
|
114
174
|
<Text size="small" weight="semibold">Event Payload</Text>
|
|
115
175
|
<Card>
|
|
116
176
|
<Stack direction="vertical" gap="small">
|
|
117
|
-
{#each getPayloadFields(event.payload) as field}
|
|
177
|
+
{#each getPayloadFields(event.payload) as field (field.label)}
|
|
118
178
|
<Text size="caption" inline>
|
|
119
179
|
<Text size="caption" weight="semibold" color="subdued" inline>{field.label}:</Text>
|
|
120
180
|
<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,19 +8,20 @@
|
|
|
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
|
-
let error
|
|
15
|
+
let error = null
|
|
13
16
|
const loading = false
|
|
14
17
|
|
|
15
18
|
let treeRenderingDone = false
|
|
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 ?? node.stepKey ?? node.stepId ?? '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 experienceLookup = client.experiences.get(historyEntry.experienceId)
|
|
10
|
+
$: experienceName = $experienceLookup?.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,46 @@
|
|
|
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
|
-
$: hasOptions =
|
|
17
|
-
$: staticOptions = Array.isArray(field?.options) ? field.options : null
|
|
18
|
+
$: hasOptions = field?.options && field.options.length > 0
|
|
18
19
|
$: currentValue = response?.value ?? null
|
|
19
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Formats a response value for display, handling arrays, objects, and primitives.
|
|
23
|
+
*
|
|
24
|
+
* @param value - The response value to format.
|
|
25
|
+
* @example
|
|
26
|
+
* ```typescript
|
|
27
|
+
* formatValue(['a', 'b']) // 'a, b'
|
|
28
|
+
* formatValue(null) // '-'
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
20
31
|
function formatValue(value: unknown): string {
|
|
21
32
|
if (value === null || value === undefined) {
|
|
22
33
|
return '-'
|
|
23
34
|
}
|
|
35
|
+
|
|
24
36
|
if (Array.isArray(value) && value.every((item) => typeof item === 'string')) {
|
|
25
37
|
return value.join(', ')
|
|
26
38
|
}
|
|
39
|
+
|
|
27
40
|
if (typeof value === 'object') {
|
|
28
41
|
return JSON.stringify(value, null, 2)
|
|
29
42
|
}
|
|
43
|
+
|
|
30
44
|
return String(value)
|
|
31
45
|
}
|
|
32
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Enters edit mode for this field, initializing the edit state from the current value.
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```typescript
|
|
52
|
+
* startEdit() // opens the inline editor
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
33
55
|
function startEdit() {
|
|
34
56
|
isEditing = true
|
|
35
57
|
editError = ''
|
|
@@ -46,6 +68,14 @@
|
|
|
46
68
|
}
|
|
47
69
|
}
|
|
48
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Cancels the current edit, discarding changes and resetting edit state.
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```typescript
|
|
76
|
+
* cancelEdit() // closes editor without saving
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
49
79
|
function cancelEdit() {
|
|
50
80
|
isEditing = false
|
|
51
81
|
editValue = ''
|
|
@@ -53,61 +83,90 @@
|
|
|
53
83
|
selectedOptions = new Set()
|
|
54
84
|
}
|
|
55
85
|
|
|
56
|
-
|
|
86
|
+
/**
|
|
87
|
+
* Toggles selection of a field option, supporting both single and multi-select modes.
|
|
88
|
+
*
|
|
89
|
+
* @param optionValue - The option value to toggle.
|
|
90
|
+
* @example
|
|
91
|
+
* ```typescript
|
|
92
|
+
* toggleOption('red') // selects or deselects the 'red' option
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
function toggleOption(optionValue: PrimitiveFieldValue) {
|
|
57
96
|
if (field?.multiple) {
|
|
58
97
|
if (selectedOptions.has(optionValue)) {
|
|
59
98
|
selectedOptions.delete(optionValue)
|
|
60
99
|
} else {
|
|
61
100
|
selectedOptions.add(optionValue)
|
|
62
101
|
}
|
|
102
|
+
|
|
63
103
|
selectedOptions = selectedOptions
|
|
64
104
|
} else {
|
|
65
105
|
selectedOptions = new Set([optionValue])
|
|
66
106
|
}
|
|
67
107
|
}
|
|
68
108
|
|
|
69
|
-
|
|
70
|
-
|
|
109
|
+
/**
|
|
110
|
+
* Parses a string input into a typed value based on the field's type.
|
|
111
|
+
*
|
|
112
|
+
* @param input - The raw string input to parse.
|
|
113
|
+
* @example
|
|
114
|
+
* ```typescript
|
|
115
|
+
* parseValue('42') // 42 (when field.type is 'number')
|
|
116
|
+
* parseValue('true') // true (when field.type is 'boolean')
|
|
117
|
+
* ```
|
|
118
|
+
*/
|
|
119
|
+
function parseValue(input: string): unknown {
|
|
120
|
+
const trimmed = input.trim()
|
|
71
121
|
|
|
72
|
-
if (
|
|
122
|
+
if (trimmed === '' || trimmed === '-') {
|
|
73
123
|
return null
|
|
74
124
|
}
|
|
75
125
|
|
|
76
|
-
if (
|
|
126
|
+
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
|
77
127
|
try {
|
|
78
|
-
return JSON.parse(
|
|
128
|
+
return JSON.parse(trimmed)
|
|
79
129
|
} catch {
|
|
80
130
|
throw new Error('Invalid JSON format')
|
|
81
131
|
}
|
|
82
132
|
}
|
|
83
133
|
|
|
84
134
|
if (field.type === 'boolean') {
|
|
85
|
-
if (
|
|
86
|
-
if (
|
|
135
|
+
if (trimmed === 'true') return true
|
|
136
|
+
if (trimmed === 'false') return false
|
|
87
137
|
throw new Error('Boolean must be "true" or "false"')
|
|
88
138
|
}
|
|
89
139
|
|
|
90
140
|
if (field.type === 'number') {
|
|
91
|
-
const num = Number(
|
|
141
|
+
const num = Number(trimmed)
|
|
92
142
|
if (isNaN(num)) {
|
|
93
143
|
throw new Error('Invalid number format')
|
|
94
144
|
}
|
|
145
|
+
|
|
95
146
|
return num
|
|
96
147
|
}
|
|
97
148
|
|
|
98
|
-
return
|
|
149
|
+
return trimmed
|
|
99
150
|
}
|
|
100
151
|
|
|
152
|
+
/**
|
|
153
|
+
* Commits the edited value via the client responses API and exits edit mode.
|
|
154
|
+
*
|
|
155
|
+
* @example
|
|
156
|
+
* ```typescript
|
|
157
|
+
* await saveEdit() // saves the current edit value
|
|
158
|
+
* ```
|
|
159
|
+
*/
|
|
101
160
|
async function saveEdit() {
|
|
102
161
|
try {
|
|
103
|
-
let newValue:
|
|
162
|
+
let newValue: unknown
|
|
104
163
|
|
|
105
164
|
if (hasOptions) {
|
|
106
165
|
const values = Array.from(selectedOptions)
|
|
107
166
|
if (field?.multiple) {
|
|
108
167
|
newValue = values
|
|
109
168
|
} else {
|
|
110
|
-
newValue = values
|
|
169
|
+
newValue = values[0] ?? null
|
|
111
170
|
}
|
|
112
171
|
} else {
|
|
113
172
|
newValue = parseValue(editValue)
|
|
@@ -128,6 +187,14 @@
|
|
|
128
187
|
}
|
|
129
188
|
}
|
|
130
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Clears the response value for this field via the client responses API.
|
|
192
|
+
*
|
|
193
|
+
* @example
|
|
194
|
+
* ```typescript
|
|
195
|
+
* handleClear() // removes the response for this field
|
|
196
|
+
* ```
|
|
197
|
+
*/
|
|
131
198
|
function handleClear() {
|
|
132
199
|
client.responses.clear(field.id)
|
|
133
200
|
}
|
|
@@ -135,7 +202,7 @@
|
|
|
135
202
|
|
|
136
203
|
<tr>
|
|
137
204
|
<td>
|
|
138
|
-
<CodeBlock code={field.
|
|
205
|
+
<CodeBlock code={field.key} inline />
|
|
139
206
|
</td>
|
|
140
207
|
<td>
|
|
141
208
|
<Text size="small" weight="semibold">{field.label}</Text>
|
|
@@ -143,9 +210,9 @@
|
|
|
143
210
|
<td>
|
|
144
211
|
{#if isEditing}
|
|
145
212
|
<Stack direction="vertical" gap="small">
|
|
146
|
-
{#if hasOptions &&
|
|
213
|
+
{#if hasOptions && field?.options}
|
|
147
214
|
<Stack direction="vertical" gap="small">
|
|
148
|
-
{#each
|
|
215
|
+
{#each field.options as option (option.id)}
|
|
149
216
|
<label class="option-label">
|
|
150
217
|
{#if field.multiple}
|
|
151
218
|
<input
|
|
@@ -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,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'
|
|
@@ -51,7 +52,7 @@
|
|
|
51
52
|
|
|
52
53
|
sample({
|
|
53
54
|
source: { steps: stepsStore, currentStep: currentStepStore },
|
|
54
|
-
fn: ({ steps, currentStep }) => steps.find((s) => s.id === currentStep?.
|
|
55
|
+
fn: ({ steps, currentStep }) => steps.find((s) => s.id === currentStep?.key) || null,
|
|
55
56
|
target: setActiveStepEvent,
|
|
56
57
|
})
|
|
57
58
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{evaluateRuleLogic as e}from"@picked/client";function t(e){return e.options?.some(e=>`condition`in e)??!1}function n(e){return e.some(e=>e.condition||e.conditionKey||t(e))}async function r(e,t){return t.conditionKey?e.conditions.getByKey(t.conditionKey).getState()?.logic??null:t.condition??null}async function i(i,a){return a.length===0||!n(a)?a:(await Promise.all(a.map(async n=>{let a=await r(i,n);if(!(!a||await e(i,a)))return null;if(!n.options||!t(n))return n;let o=(await Promise.all(n.options.map(async t=>t.condition?await e(i,t.condition)?t:null:t))).filter(e=>e!==null);return o.length===n.options.length?n:o.length===0?null:{...n,options:o}}))).filter(e=>e!==null)}export{i as getStepFields};
|
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.1774244905386",
|
|
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.1-dev.
|
|
91
|
+
"@picked/client": "0.0.1-dev.1774244905386",
|
|
92
92
|
"effector": "^23.3.0",
|
|
93
93
|
"svelte": "^5.46.4"
|
|
94
94
|
},
|
package/dist/form/create-form.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
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,onSubmit:e=>{r.user_effect(()=>s.form.onSubmit(e))},onSuccess:e=>{r.user_effect(()=>s.form.onSuccess(e))},onError:e=>{r.user_effect(()=>s.form.onError(e))}}}}export{i as createForm};
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { Store } from 'effector';
|
|
2
|
-
import { FormInstance as CoreFormInstance, ErrorCallback, SubmitCallback, SuccessCallback } from '@picked/client/form';
|
|
3
|
-
export type { FormField } from '@picked/client/form';
|
|
4
|
-
/**
|
|
5
|
-
* Svelte FormInstance — identical to core but callback registration returns void
|
|
6
|
-
* (cleanup is handled automatically via $effect, not by the consumer).
|
|
7
|
-
*/
|
|
8
|
-
export interface FormInstance<TFieldIds extends string = string> extends Omit<CoreFormInstance<TFieldIds>, 'form'> {
|
|
9
|
-
form: Omit<CoreFormInstance<TFieldIds>['form'], 'onSubmit' | 'onSuccess' | 'onError'> & {
|
|
10
|
-
onSubmit: (callback: SubmitCallback<TFieldIds>) => void;
|
|
11
|
-
onSuccess: (callback: SuccessCallback<TFieldIds>) => void;
|
|
12
|
-
onError: (callback: ErrorCallback) => void;
|
|
13
|
-
};
|
|
14
|
-
}
|
|
15
|
-
/**
|
|
16
|
-
* Creates a form instance with Svelte lifecycle management.
|
|
17
|
-
*
|
|
18
|
-
* Wraps the framework-agnostic createForm from @picked/client/form:
|
|
19
|
-
* - Injects client and experienceInstance from Svelte context
|
|
20
|
-
* - Auto-wires callback cleanup via $effect so consumers don't need to manage lifecycle
|
|
21
|
-
*
|
|
22
|
-
* Used internally by setStepContext — access via getStep() in components.
|
|
23
|
-
*/
|
|
24
|
-
export declare function createForm(formId: string, title: Store<string>, fieldIds: Store<string[]>): FormInstance;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from './create-form.js';
|
package/dist/form/get-form.d.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { ValidationError } from '@picked/client';
|
|
2
|
-
interface GetFormOptions {
|
|
3
|
-
id: string;
|
|
4
|
-
title: string;
|
|
5
|
-
onSuccess?: () => void | Promise<void>;
|
|
6
|
-
onError?: (errors: ValidationError[]) => void | Promise<void>;
|
|
7
|
-
}
|
|
8
|
-
type SubmitEventWithCurrentTarget = SubmitEvent & {
|
|
9
|
-
currentTarget: EventTarget & HTMLFormElement;
|
|
10
|
-
};
|
|
11
|
-
export declare function getForm({ id: formId, title, onError, onSuccess }: GetFormOptions): {
|
|
12
|
-
form: {
|
|
13
|
-
onsubmit: import('effector').EventCallable<SubmitEventWithCurrentTarget>;
|
|
14
|
-
};
|
|
15
|
-
};
|
|
16
|
-
export {};
|
package/dist/form/get-form.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{getExperience as e,getPicked as t}from"../hooks/client.js";import{getStep as n}from"../hooks/step.js";import"../hooks/index.js";import{buildResponsesFromFormData as r}from"@picked/client/form";import{attach as i,createEffect as a,createEvent as o,sample as s,split as c}from"effector";function l({id:l,title:u,onError:d,onSuccess:f}){let p=t(),m=n(),h=e(),{currentStepStore:g}=h,_=o(),v=a(e=>p.responses.commit(e)),y=i({source:{step:g},effect:a(({step:e})=>{p.eventBus({type:`form_submit_success`,form:{id:l,title:u},experience:h,step:e??void 0}),f?.()})}),b=i({source:{step:g},mapParams:(e,t)=>({step:t.step,errors:e.errors}),effect:a(({step:e,errors:t})=>{p.eventBus({type:`form_submit_error`,form:{id:l,title:u},exception:{reason:t},experience:h,step:e??void 0}),t.forEach(t=>{p.eventBus({type:`field_validation_error`,exception:{reason:[t]},experience:h,step:e??void 0})}),d?.(t)})});return s({clock:_,source:{step:g},fn:({step:e})=>({formId:l,title:u,step:e}),target:a(({formId:e,title:t,step:n})=>{p.eventBus({type:`form_submit`,form:{id:e,title:t},experience:h,step:n??void 0})})}),s({clock:_,source:{fields:p.fields.listStore,stepFields:m.fields,step:g},fn:({stepFields:e},t)=>(t.preventDefault(),r(e,new FormData(t.currentTarget))),target:v}),c({source:v.doneData,match:{hasErrors:({errors:e})=>e.length>0},cases:{hasErrors:b,__:y}}),{form:{onsubmit:_}}}export{l as getForm};
|