@picked/svelte 0.0.1-dev.1793776638554 → 0.0.1-dev.1793923852884
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.tsbuildinfo +1 -1
- package/dist/experience/set-picked-context.js +1 -1
- package/dist/plugins/devtools/DevToolsExperience.svelte +155 -80
- package/dist/plugins/devtools/customTools.d.ts +2 -2
- package/dist/plugins/devtools/derive/answer-display/answer-display.d.ts +14 -0
- package/dist/plugins/devtools/derive/answer-display/answer-display.js +1 -1
- package/dist/plugins/devtools/derive/answer-display/index.d.ts +2 -2
- package/dist/plugins/devtools/derive/answer-display/index.js +1 -1
- package/dist/plugins/devtools/derive/guide-sections/guide-sections.js +1 -1
- package/dist/plugins/devtools/derive/index.d.ts +5 -5
- package/dist/plugins/devtools/derive/index.js +1 -1
- package/dist/plugins/devtools/derive/personas/index.d.ts +2 -2
- package/dist/plugins/devtools/derive/personas/index.js +1 -1
- package/dist/plugins/devtools/derive/personas/personas.d.ts +88 -7
- package/dist/plugins/devtools/derive/personas/personas.js +1 -1
- package/dist/plugins/devtools/derive/plain-language/index.d.ts +1 -1
- package/dist/plugins/devtools/derive/plain-language/index.js +1 -1
- package/dist/plugins/devtools/derive/plain-language/plain-language.d.ts +5 -5
- package/dist/plugins/devtools/derive/plain-language/plain-language.js +1 -1
- package/dist/plugins/devtools/index.d.ts +1 -1
- package/dist/plugins/devtools/launcher/Launcher.svelte +13 -4
- package/dist/plugins/devtools/options.d.ts +6 -4
- package/dist/plugins/devtools/options.js +1 -1
- package/dist/plugins/devtools/review/journey/ExperienceDetails.svelte +129 -0
- package/dist/plugins/devtools/review/journey/JourneyPanel.svelte +54 -8
- package/dist/plugins/devtools/review/personas/ChoiceButton.svelte +40 -6
- package/dist/plugins/devtools/review/personas/PersonaBuilder.svelte +69 -45
- package/dist/plugins/devtools/review/personas/PersonaCard.svelte +226 -43
- package/dist/plugins/devtools/review/personas/PersonasPanel.svelte +171 -74
- package/dist/plugins/devtools/review/personas/load-persona.d.ts +41 -20
- package/dist/plugins/devtools/review/personas/load-persona.js +1 -1
- package/dist/plugins/devtools/review/personas/persona-route.d.ts +87 -0
- package/dist/plugins/devtools/review/personas/persona-route.js +1 -0
- package/dist/plugins/devtools/review/personas/persona-session.d.ts +94 -0
- package/dist/plugins/devtools/review/personas/persona-session.js +1 -0
- package/dist/plugins/devtools/sidebar/Sidebar.svelte +14 -48
- package/dist/plugins/devtools/sidebar/SidebarRow.svelte +2 -2
- package/dist/plugins/devtools/sidebar/nav.d.ts +26 -39
- package/dist/plugins/devtools/sidebar/nav.js +1 -1
- package/package.json +2 -2
- package/dist/plugins/devtools/screens/experience-instances/ExperienceInstancesListScreen.svelte +0 -10
- package/dist/plugins/devtools/screens/experiences/ExperienceDetailsScreen.svelte +0 -44
- package/dist/plugins/devtools/screens/experiences/ExperienceExperienceInstancesListScreen.svelte +0 -13
- package/dist/plugins/devtools/screens/experiences/ExperienceScreen.svelte +0 -32
- package/dist/plugins/devtools/screens/experiences/ExperienceTreeScreen.svelte +0 -500
- package/dist/plugins/devtools/screens/experiences/ExperiencesListScreen.svelte +0 -22
- package/dist/plugins/devtools/screens/features/FeaturesListScreen.svelte +0 -239
- package/dist/plugins/devtools/screens/responses/ResponseTableRow.svelte +0 -286
- package/dist/plugins/devtools/screens/responses/ResponsesListScreen.svelte +0 -12
- package/dist/plugins/devtools/screens/responses/ResponsesTable.svelte +0 -88
|
@@ -1,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 flag' },
|
|
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 feature flags below, choose values, then copy
|
|
144
|
-
the 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>
|
|
@@ -1,286 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
/** @file Editable table row for a single field response in the devtools responses screen. */
|
|
3
|
-
import type { ResponseListItem, Field, FieldValue, JsonValue } from '@picked/client'
|
|
4
|
-
import { getPicked } from '../../../../hooks'
|
|
5
|
-
import { Button, Text, CodeBlock, Banner, Stack } from '../../ui'
|
|
6
|
-
import { readOnlyReasonFromContext } from '../../posture-context'
|
|
7
|
-
|
|
8
|
-
export let field: Field
|
|
9
|
-
export let response: ResponseListItem | null
|
|
10
|
-
|
|
11
|
-
const client = getPicked()
|
|
12
|
-
const readOnlyReason = readOnlyReasonFromContext()
|
|
13
|
-
|
|
14
|
-
let isEditing = false
|
|
15
|
-
let editValue = ''
|
|
16
|
-
let editError = ''
|
|
17
|
-
// eslint-disable-next-line svelte/prefer-svelte-reactivity -- Set is reassigned for Svelte reactivity via `selectedOptions = selectedOptions`
|
|
18
|
-
let selectedOptions: Set<JsonValue> = new Set()
|
|
19
|
-
|
|
20
|
-
$: hasOptions = Array.isArray(field?.options) && field.options.length > 0
|
|
21
|
-
$: staticOptions = Array.isArray(field?.options) ? field.options : null
|
|
22
|
-
$: currentValue = response?.value ?? null
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Formats a response value for display, handling arrays, objects, and primitives.
|
|
26
|
-
*
|
|
27
|
-
* @param {unknown} value - The response value to format.
|
|
28
|
-
* @returns {string} The display string for the value.
|
|
29
|
-
* @example
|
|
30
|
-
* ```typescript
|
|
31
|
-
* formatValue(['a', 'b']) // 'a, b'
|
|
32
|
-
* formatValue(null) // '-'
|
|
33
|
-
* ```
|
|
34
|
-
*/
|
|
35
|
-
function formatValue(value: unknown): string {
|
|
36
|
-
if (value === null || value === undefined) {
|
|
37
|
-
return '-'
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (Array.isArray(value) && value.every((item) => typeof item === 'string')) {
|
|
41
|
-
return value.join(', ')
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
if (typeof value === 'object') {
|
|
45
|
-
return JSON.stringify(value, null, 2)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return String(value)
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Enters edit mode for this field, initializing the edit state from the current value.
|
|
53
|
-
*
|
|
54
|
-
* @example
|
|
55
|
-
* ```typescript
|
|
56
|
-
* startEdit() // opens the inline editor
|
|
57
|
-
* ```
|
|
58
|
-
*/
|
|
59
|
-
function startEdit() {
|
|
60
|
-
isEditing = true
|
|
61
|
-
editError = ''
|
|
62
|
-
|
|
63
|
-
if (hasOptions) {
|
|
64
|
-
selectedOptions = new Set()
|
|
65
|
-
if (Array.isArray(currentValue)) {
|
|
66
|
-
currentValue.forEach((v) => selectedOptions.add(v))
|
|
67
|
-
} else if (currentValue !== null && currentValue !== undefined) {
|
|
68
|
-
selectedOptions.add(currentValue)
|
|
69
|
-
}
|
|
70
|
-
} else {
|
|
71
|
-
editValue = formatValue(currentValue)
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Cancels the current edit, discarding changes and resetting edit state.
|
|
77
|
-
*
|
|
78
|
-
* @example
|
|
79
|
-
* ```typescript
|
|
80
|
-
* cancelEdit() // closes editor without saving
|
|
81
|
-
* ```
|
|
82
|
-
*/
|
|
83
|
-
function cancelEdit() {
|
|
84
|
-
isEditing = false
|
|
85
|
-
editValue = ''
|
|
86
|
-
editError = ''
|
|
87
|
-
selectedOptions = new Set()
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Toggles selection of a field option, supporting both single and multi-select modes.
|
|
92
|
-
*
|
|
93
|
-
* @param {JsonValue} optionValue - The option value to toggle.
|
|
94
|
-
* @example
|
|
95
|
-
* ```typescript
|
|
96
|
-
* toggleOption('red') // selects or deselects the 'red' option
|
|
97
|
-
* ```
|
|
98
|
-
*/
|
|
99
|
-
function toggleOption(optionValue: JsonValue) {
|
|
100
|
-
if (field?.multiple) {
|
|
101
|
-
if (selectedOptions.has(optionValue)) {
|
|
102
|
-
selectedOptions.delete(optionValue)
|
|
103
|
-
} else {
|
|
104
|
-
selectedOptions.add(optionValue)
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
selectedOptions = selectedOptions
|
|
108
|
-
} else {
|
|
109
|
-
selectedOptions = new Set([optionValue])
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Parses a string input into a typed value based on the field's type.
|
|
115
|
-
*
|
|
116
|
-
* @param {string} input - The raw string input to parse.
|
|
117
|
-
* @returns {FieldValue} The parsed value (null, number, boolean, JSON, or string).
|
|
118
|
-
* @example
|
|
119
|
-
* ```typescript
|
|
120
|
-
* parseValue('42') // 42 (when field.type is 'number')
|
|
121
|
-
* parseValue('true') // true (when field.type is 'boolean')
|
|
122
|
-
* ```
|
|
123
|
-
*/
|
|
124
|
-
function parseValue(input: string): FieldValue {
|
|
125
|
-
const trimmed = input.trim()
|
|
126
|
-
|
|
127
|
-
if (trimmed === '' || trimmed === '-') {
|
|
128
|
-
return null
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
|
132
|
-
try {
|
|
133
|
-
return JSON.parse(trimmed)
|
|
134
|
-
} catch {
|
|
135
|
-
throw new Error('Invalid JSON format')
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
if (field.type === 'boolean') {
|
|
140
|
-
if (trimmed === 'true') return true
|
|
141
|
-
if (trimmed === 'false') return false
|
|
142
|
-
throw new Error('Boolean must be "true" or "false"')
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if (field.type === 'number') {
|
|
146
|
-
const num = Number(trimmed)
|
|
147
|
-
if (isNaN(num)) {
|
|
148
|
-
throw new Error('Invalid number format')
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
return num
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
return trimmed
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Commits the edited value via the client responses API and exits edit mode.
|
|
159
|
-
*
|
|
160
|
-
* @example
|
|
161
|
-
* ```typescript
|
|
162
|
-
* await saveEdit() // saves the current edit value
|
|
163
|
-
* ```
|
|
164
|
-
*/
|
|
165
|
-
async function saveEdit() {
|
|
166
|
-
try {
|
|
167
|
-
let newValue: FieldValue
|
|
168
|
-
|
|
169
|
-
if (hasOptions) {
|
|
170
|
-
const values = Array.from(selectedOptions)
|
|
171
|
-
if (field?.multiple) {
|
|
172
|
-
newValue = values
|
|
173
|
-
} else {
|
|
174
|
-
newValue = values.at(0) ?? null
|
|
175
|
-
}
|
|
176
|
-
} else {
|
|
177
|
-
newValue = parseValue(editValue)
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const result = await client.responses.commit({ [field.key]: newValue })
|
|
181
|
-
|
|
182
|
-
if (result.errors.length > 0) {
|
|
183
|
-
editError = result.errors[0].userMessage
|
|
184
|
-
return
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
isEditing = false
|
|
188
|
-
editError = ''
|
|
189
|
-
selectedOptions = new Set()
|
|
190
|
-
} catch (error) {
|
|
191
|
-
editError = error instanceof Error ? error.message : 'Failed to save'
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Clears the response value for this field via the client responses API.
|
|
197
|
-
*
|
|
198
|
-
* @example
|
|
199
|
-
* ```typescript
|
|
200
|
-
* handleClear() // removes the response for this field
|
|
201
|
-
* ```
|
|
202
|
-
*/
|
|
203
|
-
function handleClear() {
|
|
204
|
-
client.responses.clear(field.key)
|
|
205
|
-
}
|
|
206
|
-
</script>
|
|
207
|
-
|
|
208
|
-
<tr>
|
|
209
|
-
<td>
|
|
210
|
-
<CodeBlock code={field.key} inline />
|
|
211
|
-
</td>
|
|
212
|
-
<td>
|
|
213
|
-
<Text size="small" weight="semibold">{field.label}</Text>
|
|
214
|
-
</td>
|
|
215
|
-
<td>
|
|
216
|
-
{#if isEditing}
|
|
217
|
-
<Stack direction="vertical" gap="small">
|
|
218
|
-
{#if hasOptions && staticOptions}
|
|
219
|
-
<Stack direction="vertical" gap="small">
|
|
220
|
-
{#each staticOptions as option (option.id)}
|
|
221
|
-
<label class="option-label">
|
|
222
|
-
{#if field.multiple}
|
|
223
|
-
<input
|
|
224
|
-
type="checkbox"
|
|
225
|
-
checked={selectedOptions.has(option.value)}
|
|
226
|
-
on:change={() => toggleOption(option.value)}
|
|
227
|
-
/>
|
|
228
|
-
{:else}
|
|
229
|
-
<input
|
|
230
|
-
type="radio"
|
|
231
|
-
name="field-{field.id}"
|
|
232
|
-
checked={selectedOptions.has(option.value)}
|
|
233
|
-
on:change={() => toggleOption(option.value)}
|
|
234
|
-
/>
|
|
235
|
-
{/if}
|
|
236
|
-
<Text size="caption" inline>
|
|
237
|
-
{option.label}
|
|
238
|
-
{#if option.description}
|
|
239
|
-
<Text size="caption" color="subdued" inline>({option.description})</Text>
|
|
240
|
-
{/if}
|
|
241
|
-
</Text>
|
|
242
|
-
</label>
|
|
243
|
-
{/each}
|
|
244
|
-
</Stack>
|
|
245
|
-
{:else}
|
|
246
|
-
<textarea
|
|
247
|
-
bind:value={editValue}
|
|
248
|
-
rows="3"
|
|
249
|
-
placeholder="Enter {field.type} value..."
|
|
250
|
-
></textarea>
|
|
251
|
-
{/if}
|
|
252
|
-
{#if editError}
|
|
253
|
-
<Banner status="error">{editError}</Banner>
|
|
254
|
-
{/if}
|
|
255
|
-
</Stack>
|
|
256
|
-
{:else if response === null}
|
|
257
|
-
<Text size="caption" color="subdued">No value set</Text>
|
|
258
|
-
{:else}
|
|
259
|
-
<CodeBlock code={formatValue(currentValue)} />
|
|
260
|
-
{/if}
|
|
261
|
-
</td>
|
|
262
|
-
<td>
|
|
263
|
-
<CodeBlock code="{field.type}{field.multiple ? '[]' : ''}" inline />
|
|
264
|
-
</td>
|
|
265
|
-
<td>
|
|
266
|
-
{#if isEditing}
|
|
267
|
-
<Stack direction="horizontal" gap="none" align="end">
|
|
268
|
-
<Button variant="primary" size="small" onclick={saveEdit}>Save</Button>
|
|
269
|
-
<Button variant="destructive" size="small" onclick={cancelEdit}>Cancel</Button>
|
|
270
|
-
</Stack>
|
|
271
|
-
{:else if readOnlyReason !== null}
|
|
272
|
-
<span title={readOnlyReason} data-testid="devtools-response-read-only">
|
|
273
|
-
<Text size="caption" color="subdued">Read-only</Text>
|
|
274
|
-
</span>
|
|
275
|
-
{:else}
|
|
276
|
-
<Stack direction="horizontal" gap="none" align="end">
|
|
277
|
-
<Button variant="secondary" size="small" onclick={startEdit}>
|
|
278
|
-
{response === null ? 'Set' : 'Edit'}
|
|
279
|
-
</Button>
|
|
280
|
-
{#if response !== null}
|
|
281
|
-
<Button variant="destructive" size="small" onclick={handleClear}>Clear</Button>
|
|
282
|
-
{/if}
|
|
283
|
-
</Stack>
|
|
284
|
-
{/if}
|
|
285
|
-
</td>
|
|
286
|
-
</tr>
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { getPicked } from '../../../../hooks'
|
|
3
|
-
import ResponsesTable from './ResponsesTable.svelte'
|
|
4
|
-
|
|
5
|
-
const client = getPicked()
|
|
6
|
-
|
|
7
|
-
$: responsesStore = client.responses.listStore
|
|
8
|
-
</script>
|
|
9
|
-
|
|
10
|
-
<div>
|
|
11
|
-
<ResponsesTable responses={$responsesStore} />
|
|
12
|
-
</div>
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
/** @file Table displaying all field responses with filtering and clear-all functionality in the devtools panel. */
|
|
3
|
-
import type { ResponseListItem } from '@picked/client'
|
|
4
|
-
import { getPicked } from '../../../../hooks'
|
|
5
|
-
import { Card, DataTable, EmptyState, Button, RadioGroup, Stack } from '../../ui'
|
|
6
|
-
import ResponseTableRow from './ResponseTableRow.svelte'
|
|
7
|
-
|
|
8
|
-
export let responses: ResponseListItem[] = []
|
|
9
|
-
|
|
10
|
-
const client = getPicked()
|
|
11
|
-
|
|
12
|
-
let filter = 'set'
|
|
13
|
-
|
|
14
|
-
const fieldsStore = client.fields.listStore
|
|
15
|
-
$: allFields = $fieldsStore ?? []
|
|
16
|
-
|
|
17
|
-
$: responseMap = new Map(responses.map((r) => [r.fieldKey, r]))
|
|
18
|
-
|
|
19
|
-
$: allFieldRows = allFields.map((field) => {
|
|
20
|
-
const response = responseMap.get(field.key)
|
|
21
|
-
return {
|
|
22
|
-
field,
|
|
23
|
-
response: response || null,
|
|
24
|
-
}
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
$: fieldRows = allFieldRows.filter((row) => {
|
|
28
|
-
if (filter === 'set') return row.response !== null
|
|
29
|
-
if (filter === 'unset') return row.response === null
|
|
30
|
-
return true
|
|
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
|
-
*/
|
|
41
|
-
function handleClearAll() {
|
|
42
|
-
client.responses.clearAll()
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const columns = [
|
|
46
|
-
{ key: 'key', header: 'Key' },
|
|
47
|
-
{ key: 'field', header: 'Field' },
|
|
48
|
-
{ key: 'value', header: 'Value' },
|
|
49
|
-
{ key: 'type', header: 'Type' },
|
|
50
|
-
{ key: 'actions', header: 'Actions' },
|
|
51
|
-
]
|
|
52
|
-
|
|
53
|
-
const filterOptions = [
|
|
54
|
-
{ value: 'all', label: 'All' },
|
|
55
|
-
{ value: 'set', label: 'Set (0)' },
|
|
56
|
-
{ value: 'unset', label: 'Unset (0)' },
|
|
57
|
-
]
|
|
58
|
-
|
|
59
|
-
$: filterOptions[1] = { value: 'set', label: `Set (${responses.length})` }
|
|
60
|
-
$: filterOptions[2] = { value: 'unset', label: `Unset (${allFields.length - responses.length})` }
|
|
61
|
-
</script>
|
|
62
|
-
|
|
63
|
-
<Stack padding="medium">
|
|
64
|
-
<Card sectioned>
|
|
65
|
-
<Stack direction="horizontal" gap="small" align="space-between" justify="center" padding="medium">
|
|
66
|
-
<RadioGroup
|
|
67
|
-
label="Show"
|
|
68
|
-
name="filter"
|
|
69
|
-
options={filterOptions}
|
|
70
|
-
value={filter}
|
|
71
|
-
onchange={(val) => { filter = val }}
|
|
72
|
-
orientation="horizontal"
|
|
73
|
-
/>
|
|
74
|
-
{#if responses.length > 0}
|
|
75
|
-
<Button variant="destructive" size="small" onclick={handleClearAll}>Clear All</Button>
|
|
76
|
-
{/if}
|
|
77
|
-
</Stack>
|
|
78
|
-
<DataTable columns={columns} rows={fieldRows}>
|
|
79
|
-
{#snippet row(item)}
|
|
80
|
-
{@const rowData = item}
|
|
81
|
-
<ResponseTableRow field={rowData.field} response={rowData.response} />
|
|
82
|
-
{/snippet}
|
|
83
|
-
{#snippet empty()}
|
|
84
|
-
<EmptyState message="No fields registered yet" />
|
|
85
|
-
{/snippet}
|
|
86
|
-
</DataTable>
|
|
87
|
-
</Card>
|
|
88
|
-
</Stack>
|