@picked/svelte 0.0.1-dev.1788925875333 → 0.0.1-next.1
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/component.d.ts +5 -5
- package/dist/component.js +1 -1
- package/dist/context.js +1 -1
- package/dist/form/index.d.ts +15 -3
- package/dist/form/index.js +1 -1
- package/dist/hooks/client.d.ts +1 -1
- package/dist/hooks/client.js +1 -1
- package/dist/hooks/field.d.ts +5 -16
- package/dist/hooks/field.js +1 -1
- package/dist/hooks/index.d.ts +1 -9
- package/dist/hooks/index.js +1 -1
- package/dist/hooks/step.d.ts +15 -93
- package/dist/hooks/step.js +1 -1
- package/dist/hooks/storage.d.ts +2 -2
- package/dist/hooks/storage.js +1 -1
- package/dist/hooks/stores.d.ts +1 -1
- package/dist/hooks/stores.js +1 -1
- package/dist/index.d.ts +1 -8
- package/dist/index.js +1 -1
- package/dist/plugins/devtools/DevToolsExperience.svelte +53 -355
- package/dist/plugins/devtools/PickedLogo.svelte +58 -0
- package/dist/plugins/devtools/components/ExperienceInstanceListItem.svelte +4 -17
- package/dist/plugins/devtools/components/ExperienceInstancesTable.svelte +19 -26
- package/dist/plugins/devtools/components/Tabs.svelte +12 -69
- package/dist/plugins/devtools/experience.d.ts +1 -0
- package/dist/plugins/devtools/experience.js +1 -0
- package/dist/plugins/devtools/index.d.ts +0 -2
- package/dist/plugins/devtools/index.js +1 -1
- package/dist/plugins/devtools/plugin.d.ts +5 -37
- package/dist/plugins/devtools/plugin.js +1 -1
- package/dist/plugins/devtools/screens/components/ComponentsListScreen.svelte +14 -18
- package/dist/plugins/devtools/screens/experience-instances/ExperienceInstancesListScreen.svelte +3 -1
- package/dist/plugins/devtools/screens/experiences/ExperienceDetailsScreen.svelte +33 -31
- package/dist/plugins/devtools/screens/experiences/ExperienceExperienceInstancesListScreen.svelte +3 -1
- package/dist/plugins/devtools/screens/experiences/ExperienceTreeScreen.svelte +103 -293
- package/dist/plugins/devtools/screens/experiences/ExperiencesListScreen.svelte +1 -2
- package/dist/plugins/devtools/screens/frameworks/FrameworksListScreen.svelte +13 -15
- package/dist/plugins/devtools/screens/history/HistoryEntriesTable.svelte +16 -22
- package/dist/plugins/devtools/screens/history/HistoryEntryListItem.svelte +1 -7
- package/dist/plugins/devtools/screens/predicates/PredicatesListScreen.svelte +13 -15
- package/dist/plugins/devtools/screens/project/ProjectScreen.svelte +1 -46
- package/dist/plugins/framework/index.js +1 -1
- package/dist/plugins/framework/plugin.d.ts +4 -4
- package/dist/plugins/framework/plugin.js +1 -1
- package/dist/render/browser.d.ts +2 -35
- package/dist/render/browser.js +1 -1
- package/dist/render/server.d.ts +8 -22
- package/dist/render/server.js +1 -1
- package/dist/router/Step.svelte +4 -6
- package/dist/router/StepRouter.svelte +33 -14
- package/dist/router/context.d.ts +3 -3
- package/dist/router/context.js +1 -1
- package/dist/router/index.js +1 -1
- package/dist/server/index.d.ts +2 -2
- package/dist/server/index.js +1 -1
- package/dist/utils/array.js +1 -1
- package/dist/vite-env.d.js +1 -0
- package/package.json +33 -106
- package/dist/.tsbuildinfo +0 -1
- package/dist/_virtual/_vite/preload-helper.js +0 -1
- package/dist/components/LazyImage.svelte +0 -192
- package/dist/components/index.d.ts +0 -1
- package/dist/components/index.js +0 -1
- package/dist/experience/ExperienceHost.svelte +0 -112
- package/dist/experience/ExperienceSlot.svelte +0 -154
- package/dist/experience/HtmlRender.svelte +0 -52
- package/dist/experience/set-picked-context.d.ts +0 -29
- package/dist/experience/set-picked-context.js +0 -1
- package/dist/form/__tests__/CreateFormHarness.svelte +0 -30
- package/dist/form/create-form.js +0 -1
- package/dist/form/create-form.svelte.d.ts +0 -40
- package/dist/form/create-form.svelte.js +0 -1
- package/dist/form/get-form.d.ts +0 -30
- package/dist/form/get-form.js +0 -1
- package/dist/hooks/action.d.ts +0 -40
- package/dist/hooks/action.js +0 -1
- package/dist/hooks/content.d.ts +0 -18
- package/dist/hooks/content.js +0 -1
- package/dist/hooks/create-query/index.d.ts +0 -145
- package/dist/hooks/create-query/index.js +0 -1
- package/dist/hooks/events.d.ts +0 -2
- package/dist/hooks/events.js +0 -1
- package/dist/hooks/feature.d.ts +0 -47
- package/dist/hooks/feature.js +0 -1
- package/dist/hooks/predicate.d.ts +0 -3
- package/dist/hooks/predicate.js +0 -1
- package/dist/hooks/reference.d.ts +0 -61
- package/dist/hooks/reference.js +0 -1
- package/dist/plugins/devtools/PickedIcon.svelte +0 -19
- package/dist/plugins/devtools/components/AdaptedToolHost.svelte +0 -103
- package/dist/plugins/devtools/customTools.d.ts +0 -27
- package/dist/plugins/devtools/customTools.js +0 -1
- package/dist/plugins/devtools/screens/config/ConfigScreen.svelte +0 -160
- package/dist/plugins/devtools/screens/content/ContentListScreen.svelte +0 -89
- package/dist/plugins/devtools/screens/custom-tools/CustomToolsScreen.svelte +0 -29
- package/dist/plugins/devtools/screens/events/EventListItem.svelte +0 -216
- package/dist/plugins/devtools/screens/events/EventsListScreen.svelte +0 -18
- package/dist/plugins/devtools/screens/events/EventsTable.svelte +0 -76
- package/dist/plugins/devtools/screens/features/FeaturesListScreen.svelte +0 -239
- package/dist/plugins/devtools/screens/fields/FieldTableRow.svelte +0 -276
- package/dist/plugins/devtools/screens/fields/FieldsListScreen.svelte +0 -12
- package/dist/plugins/devtools/screens/fields/FieldsTable.svelte +0 -78
- package/dist/plugins/devtools/screens/identity/IdentityScreen.svelte +0 -66
- package/dist/plugins/devtools/screens/responses/ResponseTableRow.svelte +0 -280
- package/dist/plugins/devtools/screens/responses/ResponsesListScreen.svelte +0 -12
- package/dist/plugins/devtools/screens/responses/ResponsesTable.svelte +0 -88
- package/dist/plugins/devtools/ui/actions/Button.svelte +0 -175
- package/dist/plugins/devtools/ui/actions/CopyButton.svelte +0 -88
- package/dist/plugins/devtools/ui/data/Badge.svelte +0 -62
- package/dist/plugins/devtools/ui/data/CodeBlock.svelte +0 -38
- package/dist/plugins/devtools/ui/data/DataTable.svelte +0 -144
- package/dist/plugins/devtools/ui/data/EmptyState.svelte +0 -26
- package/dist/plugins/devtools/ui/data/ExpandableRow.svelte +0 -66
- package/dist/plugins/devtools/ui/data/VisualizationContainer.svelte +0 -55
- package/dist/plugins/devtools/ui/feedback/Banner.svelte +0 -116
- package/dist/plugins/devtools/ui/feedback/Spinner.svelte +0 -79
- package/dist/plugins/devtools/ui/forms/Checkbox.svelte +0 -139
- package/dist/plugins/devtools/ui/forms/RadioGroup.svelte +0 -218
- package/dist/plugins/devtools/ui/forms/Select.svelte +0 -137
- package/dist/plugins/devtools/ui/forms/Switch.svelte +0 -158
- package/dist/plugins/devtools/ui/forms/TextField.svelte +0 -125
- package/dist/plugins/devtools/ui/index.d.ts +0 -22
- package/dist/plugins/devtools/ui/index.js +0 -1
- package/dist/plugins/devtools/ui/layout/Card.svelte +0 -55
- package/dist/plugins/devtools/ui/layout/Divider.svelte +0 -34
- package/dist/plugins/devtools/ui/layout/Spacer.svelte +0 -11
- package/dist/plugins/devtools/ui/layout/Stack.svelte +0 -129
- package/dist/plugins/devtools/ui/overlays/Tooltip.svelte +0 -90
- package/dist/plugins/devtools/ui/tokens.css +0 -144
- package/dist/plugins/devtools/ui/typography/Heading.svelte +0 -55
- package/dist/plugins/devtools/ui/typography/Text.svelte +0 -93
- package/dist/utils/ErrorBoundary.svelte +0 -27
|
@@ -1,216 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
/** @file Expandable list item row for displaying an event bus history entry in the devtools events screen. */
|
|
3
|
-
import type { EventBusPayload, EventHistoryEntry } from '@picked/client'
|
|
4
|
-
import { Badge, Card, CodeBlock, Stack, Text } from '../../ui'
|
|
5
|
-
import { getPicked } from '../../../../hooks'
|
|
6
|
-
|
|
7
|
-
export let event: EventHistoryEntry
|
|
8
|
-
export let isExpanded: boolean
|
|
9
|
-
export let onToggle: () => void
|
|
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
|
-
*/
|
|
32
|
-
function formatTime(date: Date): string {
|
|
33
|
-
return date.toLocaleTimeString('en-US', {
|
|
34
|
-
hour12: false,
|
|
35
|
-
hour: '2-digit',
|
|
36
|
-
minute: '2-digit',
|
|
37
|
-
second: '2-digit',
|
|
38
|
-
fractionalSecondDigits: 3,
|
|
39
|
-
})
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
interface PayloadField {
|
|
43
|
-
label: string
|
|
44
|
-
value: unknown
|
|
45
|
-
}
|
|
46
|
-
|
|
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 []
|
|
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
|
-
}
|
|
66
|
-
|
|
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
|
-
}
|
|
82
|
-
|
|
83
|
-
const fields: PayloadField[] = []
|
|
84
|
-
if ('key' in step && step.key) {
|
|
85
|
-
fields.push({ label: 'Step Key', value: step.key })
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if ('name' in step && step.name) {
|
|
89
|
-
fields.push({ label: 'Step Name', value: step.name })
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return fields
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/** Scalar payload keys to extract and their display labels. JSON-serializable values are stringified. */
|
|
96
|
-
const SCALAR_KEYS: { key: string; label: string; serialize?: boolean }[] = [
|
|
97
|
-
{ key: 'value', label: 'Value' },
|
|
98
|
-
{ key: 'form', label: 'Form', serialize: true },
|
|
99
|
-
{ key: 'feature', label: 'Feature', serialize: true },
|
|
100
|
-
{ key: 'exception', label: 'Exception', serialize: true },
|
|
101
|
-
{ key: 'version', label: 'Version' },
|
|
102
|
-
]
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Extracts scalar (non-nested) fields from an event bus payload.
|
|
106
|
-
*
|
|
107
|
-
* @param {EventBusPayload} payload - The event bus payload to extract scalar fields from.
|
|
108
|
-
* @returns {PayloadField[]} Extracted scalar fields.
|
|
109
|
-
* @example
|
|
110
|
-
* ```typescript
|
|
111
|
-
* getScalarFields({ type: 'step', value: 42 })
|
|
112
|
-
* // [{ label: 'Value', value: 42 }]
|
|
113
|
-
* ```
|
|
114
|
-
*/
|
|
115
|
-
function getScalarFields(payload: EventBusPayload): PayloadField[] {
|
|
116
|
-
const fields: PayloadField[] = []
|
|
117
|
-
const record = payload as Record<string, unknown>
|
|
118
|
-
|
|
119
|
-
for (const { key, label, serialize } of SCALAR_KEYS) {
|
|
120
|
-
const raw = record[key]
|
|
121
|
-
if (raw !== undefined) {
|
|
122
|
-
fields.push({ label, value: serialize ? JSON.stringify(raw) : raw })
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
return fields
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Builds a flat list of display fields from an event bus payload.
|
|
131
|
-
*
|
|
132
|
-
* @param {EventBusPayload} payload - The event bus payload to extract fields from.
|
|
133
|
-
* @returns {PayloadField[]} All extracted fields.
|
|
134
|
-
* @example
|
|
135
|
-
* ```typescript
|
|
136
|
-
* getPayloadFields({ type: 'session', id: 'sess-1' })
|
|
137
|
-
* // [{ label: 'Type', value: 'session' }, { label: 'Session ID', value: 'sess-1' }]
|
|
138
|
-
* ```
|
|
139
|
-
*/
|
|
140
|
-
function getPayloadFields(payload: EventBusPayload): PayloadField[] {
|
|
141
|
-
const fields: PayloadField[] = [{ label: 'Type', value: payload.type }]
|
|
142
|
-
fields.push(...getScalarFields(payload))
|
|
143
|
-
|
|
144
|
-
if ('experience' in payload && payload.experience) {
|
|
145
|
-
fields.push(...getExperienceFields(payload.experience))
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
if ('step' in payload && payload.step) {
|
|
149
|
-
fields.push(...getStepFields(payload.step))
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
if ('id' in payload && payload.id && payload.type === 'session') {
|
|
153
|
-
fields.push({ label: 'Session ID', value: payload.id })
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
return fields
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Formats a field value for display as a string.
|
|
161
|
-
*
|
|
162
|
-
* @param {unknown} value - The value to format.
|
|
163
|
-
* @returns {string} The formatted string representation.
|
|
164
|
-
* @example
|
|
165
|
-
* ```typescript
|
|
166
|
-
* formatValue(42) // '42'
|
|
167
|
-
* formatValue(null) // 'null'
|
|
168
|
-
* ```
|
|
169
|
-
*/
|
|
170
|
-
function formatValue(value: unknown): string {
|
|
171
|
-
if (value === null) return 'null'
|
|
172
|
-
if (value === undefined) return 'undefined'
|
|
173
|
-
if (typeof value === 'string') return value
|
|
174
|
-
if (typeof value === 'number' || typeof value === 'boolean') return String(value)
|
|
175
|
-
return JSON.stringify(value)
|
|
176
|
-
}
|
|
177
|
-
</script>
|
|
178
|
-
|
|
179
|
-
<tr class="event-row" onclick={onToggle}>
|
|
180
|
-
<td>
|
|
181
|
-
<span class="expand-icon">
|
|
182
|
-
{isExpanded ? '▼' : '▶'}
|
|
183
|
-
</span>
|
|
184
|
-
<CodeBlock code={formatTime(event.at)} inline />
|
|
185
|
-
</td>
|
|
186
|
-
<td>
|
|
187
|
-
<Badge status="info">{event.payload.type}</Badge>
|
|
188
|
-
</td>
|
|
189
|
-
<td>
|
|
190
|
-
<Text size="small">{eventExpName}</Text>
|
|
191
|
-
</td>
|
|
192
|
-
<td>
|
|
193
|
-
<Text size="caption" color="subdued">
|
|
194
|
-
{isExpanded ? 'Click to collapse' : 'Click to expand payload'}
|
|
195
|
-
</Text>
|
|
196
|
-
</td>
|
|
197
|
-
</tr>
|
|
198
|
-
{#if isExpanded}
|
|
199
|
-
<tr>
|
|
200
|
-
<td colspan="4" class="details-cell">
|
|
201
|
-
<Stack direction="vertical" gap="small">
|
|
202
|
-
<Text size="small" weight="semibold">Event Payload</Text>
|
|
203
|
-
<Card>
|
|
204
|
-
<Stack direction="vertical" gap="small">
|
|
205
|
-
{#each getPayloadFields(event.payload) as field (field.label)}
|
|
206
|
-
<Text size="caption" inline>
|
|
207
|
-
<Text size="caption" weight="semibold" color="subdued" inline>{field.label}:</Text>
|
|
208
|
-
<CodeBlock code={formatValue(field.value)} inline />
|
|
209
|
-
</Text>
|
|
210
|
-
{/each}
|
|
211
|
-
</Stack>
|
|
212
|
-
</Card>
|
|
213
|
-
</Stack>
|
|
214
|
-
</td>
|
|
215
|
-
</tr>
|
|
216
|
-
{/if}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { getPicked } from '../../../../hooks'
|
|
3
|
-
import EventsTable from './EventsTable.svelte'
|
|
4
|
-
import { Divider, Spacer, Stack, Text } from '../../ui'
|
|
5
|
-
|
|
6
|
-
const { events } = getPicked()
|
|
7
|
-
</script>
|
|
8
|
-
|
|
9
|
-
<Stack direction="vertical" gap="none" fill>
|
|
10
|
-
<EventsTable />
|
|
11
|
-
<Spacer />
|
|
12
|
-
<Divider />
|
|
13
|
-
<Stack padding="small">
|
|
14
|
-
<Text size="caption" color="subdued">
|
|
15
|
-
{$events.length} events
|
|
16
|
-
</Text>
|
|
17
|
-
</Stack>
|
|
18
|
-
</Stack>
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { combine, createStore, createEvent, sample } from 'effector'
|
|
3
|
-
import { getPicked } from '../../../../hooks'
|
|
4
|
-
import EventListItem from './EventListItem.svelte'
|
|
5
|
-
import { Card, DataTable, EmptyState, Stack } from '../../ui'
|
|
6
|
-
import type { EventHistoryEntry } from '@picked/client'
|
|
7
|
-
|
|
8
|
-
const { events } = getPicked()
|
|
9
|
-
|
|
10
|
-
let expandedRowIds = new Set<number>()
|
|
11
|
-
|
|
12
|
-
function toggleRow(eventId: number) {
|
|
13
|
-
const newSet = new Set(expandedRowIds)
|
|
14
|
-
if (newSet.has(eventId)) {
|
|
15
|
-
newSet.delete(eventId)
|
|
16
|
-
} else {
|
|
17
|
-
newSet.add(eventId)
|
|
18
|
-
}
|
|
19
|
-
expandedRowIds = newSet
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const sortDirectionStore = createStore<'asc' | 'desc'>('desc')
|
|
23
|
-
const toggleSortDirection = createEvent<void>()
|
|
24
|
-
const oppositeSortDirection: Record<'asc' | 'desc', 'asc' | 'desc'> = { asc: 'desc', desc: 'asc' }
|
|
25
|
-
|
|
26
|
-
sample({
|
|
27
|
-
clock: toggleSortDirection,
|
|
28
|
-
source: sortDirectionStore,
|
|
29
|
-
fn: (dir) => oppositeSortDirection[dir],
|
|
30
|
-
target: sortDirectionStore,
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
function sortEvents(history: EventHistoryEntry[], sort: 'asc' | 'desc'): EventHistoryEntry[] {
|
|
34
|
-
return [...history].sort((a, b) => {
|
|
35
|
-
const diff = a.at.getTime() - b.at.getTime()
|
|
36
|
-
return sort === 'asc' ? diff : -diff
|
|
37
|
-
})
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const sortedEvents = combine(events, sortDirectionStore, sortEvents)
|
|
41
|
-
|
|
42
|
-
const columns = [
|
|
43
|
-
{ key: 'time', header: 'Time', sortable: true },
|
|
44
|
-
{ key: 'eventType', header: 'Event Type' },
|
|
45
|
-
{ key: 'experience', header: 'Experience' },
|
|
46
|
-
{ key: 'actions', header: '' },
|
|
47
|
-
]
|
|
48
|
-
|
|
49
|
-
function handleSort() {
|
|
50
|
-
toggleSortDirection()
|
|
51
|
-
}
|
|
52
|
-
</script>
|
|
53
|
-
|
|
54
|
-
<Stack padding="medium">
|
|
55
|
-
<Card sectioned>
|
|
56
|
-
<DataTable
|
|
57
|
-
columns={columns}
|
|
58
|
-
rows={$sortedEvents}
|
|
59
|
-
sortKey="time"
|
|
60
|
-
sortDirection={$sortDirectionStore}
|
|
61
|
-
onSort={handleSort}
|
|
62
|
-
>
|
|
63
|
-
{#snippet row(item)}
|
|
64
|
-
{@const event = item}
|
|
65
|
-
<EventListItem
|
|
66
|
-
{event}
|
|
67
|
-
isExpanded={expandedRowIds.has(event.id)}
|
|
68
|
-
onToggle={() => toggleRow(event.id)}
|
|
69
|
-
/>
|
|
70
|
-
{/snippet}
|
|
71
|
-
{#snippet empty()}
|
|
72
|
-
<EmptyState message="No events yet. Interact with the page to see events." />
|
|
73
|
-
{/snippet}
|
|
74
|
-
</DataTable>
|
|
75
|
-
</Card>
|
|
76
|
-
</Stack>
|
|
@@ -1,239 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { createEvent, createStore } from 'effector'
|
|
3
|
-
import type { FeatureBucket } from '@picked/client'
|
|
4
|
-
import { getPicked, useUnit } from '../../../../hooks'
|
|
5
|
-
import { Button, Card, CodeBlock, CopyButton, DataTable, EmptyState, Stack, Text } from '../../ui'
|
|
6
|
-
|
|
7
|
-
interface FeatureSelection {
|
|
8
|
-
included: boolean
|
|
9
|
-
value: string
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/** A boolean feature's two arms. */
|
|
13
|
-
const BOOLEAN_VALUES = ['true', 'false']
|
|
14
|
-
|
|
15
|
-
/** What every feature's control arm serves. */
|
|
16
|
-
const CONTROL_VALUE = 'false'
|
|
17
|
-
|
|
18
|
-
const { features: { listStore: featuresStore } } = getPicked()
|
|
19
|
-
const featuresList = useUnit(featuresStore)
|
|
20
|
-
|
|
21
|
-
const initSelections = createEvent<FeatureBucket[]>()
|
|
22
|
-
const featureToggled = createEvent<string>()
|
|
23
|
-
const valueChanged = createEvent<{ id: string; value: string }>()
|
|
24
|
-
|
|
25
|
-
const selectionsStore = createStore<Record<string, FeatureSelection>>({})
|
|
26
|
-
.on(initSelections, (state, feats) => {
|
|
27
|
-
const next = { ...state }
|
|
28
|
-
feats.forEach((feature) => {
|
|
29
|
-
if (!(feature.id in next)) {
|
|
30
|
-
next[feature.id] = { included: false, value: defaultValue(feature) }
|
|
31
|
-
}
|
|
32
|
-
})
|
|
33
|
-
return next
|
|
34
|
-
})
|
|
35
|
-
.on(featureToggled, (state, id) => ({
|
|
36
|
-
...state,
|
|
37
|
-
[id]: { ...state[id], included: !state[id].included },
|
|
38
|
-
}))
|
|
39
|
-
.on(valueChanged, (state, { id, value }) => ({
|
|
40
|
-
...state,
|
|
41
|
-
[id]: { ...state[id], value },
|
|
42
|
-
}))
|
|
43
|
-
|
|
44
|
-
const selections = useUnit(selectionsStore)
|
|
45
|
-
|
|
46
|
-
$effect(() => {
|
|
47
|
-
initSelections($featuresList)
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* The bucket a feature currently serves, or null while it is still undecided.
|
|
52
|
-
*
|
|
53
|
-
* @param {FeatureBucket} feature - The feature to read.
|
|
54
|
-
* @returns {string | null} The bucket as text, or null when the feature is unresolved.
|
|
55
|
-
* @example getBucketValue(feature) // 'video-hero'
|
|
56
|
-
*/
|
|
57
|
-
function getBucketValue(feature: FeatureBucket): string | null {
|
|
58
|
-
return feature.isResolved ? String(feature.getBucket().bucket) : null
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* The buckets a feature can be forced to: a text feature's published arms, or the
|
|
63
|
-
* two structural arms of a boolean one. A feature of unknown shape answers null,
|
|
64
|
-
* and the caller takes free text.
|
|
65
|
-
*
|
|
66
|
-
* @param {FeatureBucket} feature - The feature to offer overrides for.
|
|
67
|
-
* @returns {string[] | null} The values to list, or null for a feature of unknown shape.
|
|
68
|
-
* @example overrideValues(feature) // ['video-hero', 'false']
|
|
69
|
-
*/
|
|
70
|
-
function overrideValues(feature: FeatureBucket): string[] | null {
|
|
71
|
-
const variants = feature.variants
|
|
72
|
-
if (variants && variants.length > 0) {
|
|
73
|
-
return variants.map((variant) => String(variant.value))
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// A text feature with no arms, and a feature resolved outside `true`/`false`, are
|
|
77
|
-
// both of unknown shape.
|
|
78
|
-
const current = getBucketValue(feature)
|
|
79
|
-
if (feature.valueType === 'text' || (current !== null && !BOOLEAN_VALUES.includes(current))) {
|
|
80
|
-
return null
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return BOOLEAN_VALUES
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* The bucket a feature's override starts on: whatever it currently serves, else its
|
|
88
|
-
* first arm outside control, so one click on Include forces the treatment.
|
|
89
|
-
*
|
|
90
|
-
* @param {FeatureBucket} feature - The feature the selection is being seeded for.
|
|
91
|
-
* @returns {string} The bucket to preselect, or the empty string when there is none to offer.
|
|
92
|
-
* @example defaultValue(feature) // 'video-hero'
|
|
93
|
-
*/
|
|
94
|
-
function defaultValue(feature: FeatureBucket): string {
|
|
95
|
-
const current = getBucketValue(feature)
|
|
96
|
-
if (current !== null) {
|
|
97
|
-
return current
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const values = overrideValues(feature)
|
|
101
|
-
if (!values || values.length === 0) {
|
|
102
|
-
return ''
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
return values.find((value) => value !== CONTROL_VALUE) ?? values[0]
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const generatedUrl = $derived.by(() => {
|
|
109
|
-
const url = new URL(window.location.href)
|
|
110
|
-
url.searchParams.delete('picked-feature')
|
|
111
|
-
// `searchParams` escapes a value holding a colon or an ampersand, which is what
|
|
112
|
-
// keeps the round trip exact.
|
|
113
|
-
$featuresList
|
|
114
|
-
.filter((feature) => $selections[feature.id]?.included)
|
|
115
|
-
.forEach((feature) => {
|
|
116
|
-
url.searchParams.append('picked-feature', `${feature.id}:${$selections[feature.id].value}`)
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
return url.toString()
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
const hasSelections = $derived(Object.values($selections).some((s) => s.included))
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Navigates to the generated URL, which loads this page with the overrides applied.
|
|
126
|
-
*
|
|
127
|
-
* @example refreshPage()
|
|
128
|
-
*/
|
|
129
|
-
function refreshPage() {
|
|
130
|
-
window.location.href = generatedUrl
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const columns = [
|
|
134
|
-
{ key: 'include', header: 'Include' },
|
|
135
|
-
{ key: 'feature', header: 'Feature' },
|
|
136
|
-
{ key: 'currentValue', header: 'Current Value' },
|
|
137
|
-
{ key: 'overrideValue', header: 'Override Value' },
|
|
138
|
-
]
|
|
139
|
-
</script>
|
|
140
|
-
|
|
141
|
-
<Stack direction="vertical" gap="medium" padding="medium">
|
|
142
|
-
<Text size="caption" color="subdued">
|
|
143
|
-
Override feature flags via URL parameters. Select features below, choose values, then copy the
|
|
144
|
-
generated URL. Recipients will see the overridden values on page load.
|
|
145
|
-
</Text>
|
|
146
|
-
|
|
147
|
-
<Card sectioned>
|
|
148
|
-
<DataTable columns={columns} rows={$featuresList}>
|
|
149
|
-
{#snippet row(item)}
|
|
150
|
-
{@const feature = item}
|
|
151
|
-
{@const selection = $selections[feature.id]}
|
|
152
|
-
{@const currentValue = getBucketValue(feature)}
|
|
153
|
-
{@const values = overrideValues(feature)}
|
|
154
|
-
{#if selection}
|
|
155
|
-
<tr class:selected={selection.included}>
|
|
156
|
-
<td class="include-cell">
|
|
157
|
-
<input
|
|
158
|
-
type="checkbox"
|
|
159
|
-
data-feature-id={feature.id}
|
|
160
|
-
checked={selection.included}
|
|
161
|
-
onchange={() => featureToggled(feature.id)}
|
|
162
|
-
aria-label="Include {feature.name} in URL"
|
|
163
|
-
/>
|
|
164
|
-
</td>
|
|
165
|
-
<td>
|
|
166
|
-
<Text size="small" weight="medium">{feature.name}</Text>
|
|
167
|
-
<CodeBlock code={`${feature.id}:${feature.key}`} inline />
|
|
168
|
-
</td>
|
|
169
|
-
<td>
|
|
170
|
-
{#if currentValue !== null}
|
|
171
|
-
<CodeBlock code={currentValue} inline />
|
|
172
|
-
{:else}
|
|
173
|
-
<Button variant="ghost" size="small" onclick={() => feature.getBucket()}>
|
|
174
|
-
Click to resolve
|
|
175
|
-
</Button>
|
|
176
|
-
{/if}
|
|
177
|
-
</td>
|
|
178
|
-
<td>
|
|
179
|
-
{#if values}
|
|
180
|
-
<select
|
|
181
|
-
class="inline-select"
|
|
182
|
-
data-override-for={feature.id}
|
|
183
|
-
value={selection.value}
|
|
184
|
-
onchange={(e) => valueChanged({ id: feature.id, value: e.currentTarget.value })}
|
|
185
|
-
disabled={!selection.included}
|
|
186
|
-
aria-label="Override value for {feature.name}"
|
|
187
|
-
>
|
|
188
|
-
<!-- Keyed by position: two arms may serve the same string. -->
|
|
189
|
-
{#each values as value, index (index)}
|
|
190
|
-
<option {value}>{value}</option>
|
|
191
|
-
{/each}
|
|
192
|
-
</select>
|
|
193
|
-
{:else}
|
|
194
|
-
<input
|
|
195
|
-
class="inline-input"
|
|
196
|
-
type="text"
|
|
197
|
-
data-override-for={feature.id}
|
|
198
|
-
value={selection.value}
|
|
199
|
-
oninput={(e) => valueChanged({ id: feature.id, value: e.currentTarget.value })}
|
|
200
|
-
disabled={!selection.included}
|
|
201
|
-
placeholder="value"
|
|
202
|
-
aria-label="Override value for {feature.name}"
|
|
203
|
-
/>
|
|
204
|
-
{/if}
|
|
205
|
-
</td>
|
|
206
|
-
</tr>
|
|
207
|
-
{/if}
|
|
208
|
-
{/snippet}
|
|
209
|
-
{#snippet empty()}
|
|
210
|
-
<EmptyState message="No features registered" />
|
|
211
|
-
{/snippet}
|
|
212
|
-
</DataTable>
|
|
213
|
-
</Card>
|
|
214
|
-
|
|
215
|
-
<Card>
|
|
216
|
-
{#snippet header()}
|
|
217
|
-
<Text size="caption" weight="semibold" color="subdued">GENERATED URL</Text>
|
|
218
|
-
{/snippet}
|
|
219
|
-
<Stack direction="vertical" gap="small">
|
|
220
|
-
<CodeBlock code={generatedUrl} />
|
|
221
|
-
<Stack direction="horizontal" gap="small" justify="center">
|
|
222
|
-
<CopyButton text={generatedUrl} label="Copy URL" />
|
|
223
|
-
<Button
|
|
224
|
-
variant="secondary"
|
|
225
|
-
size="small"
|
|
226
|
-
onclick={refreshPage}
|
|
227
|
-
disabled={!hasSelections}
|
|
228
|
-
>
|
|
229
|
-
Reload
|
|
230
|
-
</Button>
|
|
231
|
-
</Stack>
|
|
232
|
-
{#if !hasSelections}
|
|
233
|
-
<Text size="caption" color="subdued">
|
|
234
|
-
Select at least one feature above to add overrides to the URL.
|
|
235
|
-
</Text>
|
|
236
|
-
{/if}
|
|
237
|
-
</Stack>
|
|
238
|
-
</Card>
|
|
239
|
-
</Stack>
|