@picked/svelte 0.0.37 → 0.0.38-next.2
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/plugins/devtools/DevToolsExperience.svelte +49 -0
- package/dist/plugins/devtools/screens/responses/ResponseTableRow.svelte +247 -0
- package/dist/plugins/devtools/screens/responses/ResponsesListScreen.svelte +12 -0
- package/dist/plugins/devtools/screens/responses/ResponsesTable.svelte +98 -0
- package/package.json +2 -2
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import FrameworksListScreen from './screens/frameworks/FrameworksListScreen.svelte'
|
|
10
10
|
import ProjectScreen from './screens/project/ProjectScreen.svelte'
|
|
11
11
|
import HistoryEntriesListScreen from './screens/history/HistoryEntriesListScreen.svelte'
|
|
12
|
+
import ResponsesListScreen from './screens/responses/ResponsesListScreen.svelte'
|
|
12
13
|
|
|
13
14
|
const experience = getExperience()
|
|
14
15
|
|
|
@@ -25,6 +26,12 @@
|
|
|
25
26
|
component: HistoryEntriesListScreen,
|
|
26
27
|
props: {},
|
|
27
28
|
},
|
|
29
|
+
{
|
|
30
|
+
id: 'responses',
|
|
31
|
+
label: 'Responses',
|
|
32
|
+
component: ResponsesListScreen,
|
|
33
|
+
props: {},
|
|
34
|
+
},
|
|
28
35
|
{
|
|
29
36
|
id: 'experience-instances',
|
|
30
37
|
label: 'Experience Instances',
|
|
@@ -79,6 +86,15 @@
|
|
|
79
86
|
</section>
|
|
80
87
|
</div>
|
|
81
88
|
</div>
|
|
89
|
+
{:else}
|
|
90
|
+
<button
|
|
91
|
+
class="floating-button"
|
|
92
|
+
on:click={() => experience.open()}
|
|
93
|
+
aria-label="Open Picked Devtools"
|
|
94
|
+
title="Open Picked Devtools (or press .)"
|
|
95
|
+
>
|
|
96
|
+
<PickedLogo />
|
|
97
|
+
</button>
|
|
82
98
|
{/if}
|
|
83
99
|
|
|
84
100
|
<style>
|
|
@@ -166,4 +182,37 @@
|
|
|
166
182
|
font-weight: normal;
|
|
167
183
|
background-color: #f0f0f0;
|
|
168
184
|
}
|
|
185
|
+
|
|
186
|
+
.floating-button {
|
|
187
|
+
position: fixed;
|
|
188
|
+
bottom: 1.5rem;
|
|
189
|
+
right: 1.5rem;
|
|
190
|
+
width: 4.5rem;
|
|
191
|
+
height: 4.5rem;
|
|
192
|
+
border-radius: 50%;
|
|
193
|
+
background-color: white;
|
|
194
|
+
border: 1px solid lightgray;
|
|
195
|
+
box-shadow: 0 4px 12px rgba(21, 73, 255, 0.2);
|
|
196
|
+
display: flex;
|
|
197
|
+
align-items: center;
|
|
198
|
+
justify-content: center;
|
|
199
|
+
cursor: pointer;
|
|
200
|
+
z-index: 999999;
|
|
201
|
+
transition: all 0.2s ease;
|
|
202
|
+
padding: 0;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.floating-button:hover {
|
|
206
|
+
box-shadow: 0 6px 16px rgba(21, 73, 255, 0.3);
|
|
207
|
+
transform: scale(1.05);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.floating-button:active {
|
|
211
|
+
transform: scale(0.95);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.floating-button :global(svg) {
|
|
215
|
+
width: 2.25rem;
|
|
216
|
+
height: 2.25rem;
|
|
217
|
+
}
|
|
169
218
|
</style>
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ResponseListItem, PrimitiveFieldValue, Field } from '@picked/client'
|
|
3
|
+
import { getPicked } from '../../../../hooks'
|
|
4
|
+
|
|
5
|
+
export let field: Field
|
|
6
|
+
export let response: ResponseListItem | null
|
|
7
|
+
|
|
8
|
+
const client = getPicked()
|
|
9
|
+
|
|
10
|
+
let isEditing = false
|
|
11
|
+
let editValue = ''
|
|
12
|
+
let editError = ''
|
|
13
|
+
let selectedOptions: Set<PrimitiveFieldValue> = new Set()
|
|
14
|
+
|
|
15
|
+
$: hasOptions = field?.options && field.options.length > 0
|
|
16
|
+
$: currentValue = response?.value ?? null
|
|
17
|
+
|
|
18
|
+
function formatValue(value: unknown): string {
|
|
19
|
+
if (value === null || value === undefined) {
|
|
20
|
+
return '-'
|
|
21
|
+
}
|
|
22
|
+
if (Array.isArray(value) && value.every((item) => typeof item === 'string')) {
|
|
23
|
+
return value.join(', ')
|
|
24
|
+
}
|
|
25
|
+
if (typeof value === 'object') {
|
|
26
|
+
return JSON.stringify(value, null, 2)
|
|
27
|
+
}
|
|
28
|
+
return String(value)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function startEdit() {
|
|
32
|
+
isEditing = true
|
|
33
|
+
editError = ''
|
|
34
|
+
|
|
35
|
+
if (hasOptions) {
|
|
36
|
+
// Initialize selected options from current value
|
|
37
|
+
selectedOptions = new Set()
|
|
38
|
+
if (Array.isArray(currentValue)) {
|
|
39
|
+
currentValue.forEach((v) => selectedOptions.add(v))
|
|
40
|
+
} else if (currentValue !== null && currentValue !== undefined) {
|
|
41
|
+
selectedOptions.add(currentValue)
|
|
42
|
+
}
|
|
43
|
+
} else {
|
|
44
|
+
editValue = formatValue(currentValue)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function cancelEdit() {
|
|
49
|
+
isEditing = false
|
|
50
|
+
editValue = ''
|
|
51
|
+
editError = ''
|
|
52
|
+
selectedOptions = new Set()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function toggleOption(optionValue: PrimitiveFieldValue) {
|
|
56
|
+
if (field?.multiple) {
|
|
57
|
+
// Multiple selection: toggle in set
|
|
58
|
+
if (selectedOptions.has(optionValue)) {
|
|
59
|
+
selectedOptions.delete(optionValue)
|
|
60
|
+
} else {
|
|
61
|
+
selectedOptions.add(optionValue)
|
|
62
|
+
}
|
|
63
|
+
selectedOptions = selectedOptions // Trigger reactivity
|
|
64
|
+
} else {
|
|
65
|
+
// Single selection: replace set
|
|
66
|
+
selectedOptions = new Set([optionValue])
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function parseValue(input: string): unknown {
|
|
71
|
+
input = input.trim()
|
|
72
|
+
|
|
73
|
+
// Handle empty input
|
|
74
|
+
if (input === '' || input === '-') {
|
|
75
|
+
return null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Try to parse as JSON first for arrays and objects
|
|
79
|
+
if (input.startsWith('{') || input.startsWith('[')) {
|
|
80
|
+
try {
|
|
81
|
+
return JSON.parse(input)
|
|
82
|
+
} catch {
|
|
83
|
+
throw new Error('Invalid JSON format')
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Handle specific types based on field type
|
|
88
|
+
if (field.type === 'boolean') {
|
|
89
|
+
if (input === 'true') return true
|
|
90
|
+
if (input === 'false') return false
|
|
91
|
+
throw new Error('Boolean must be "true" or "false"')
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (field.type === 'number') {
|
|
95
|
+
const num = Number(input)
|
|
96
|
+
if (isNaN(num)) {
|
|
97
|
+
throw new Error('Invalid number format')
|
|
98
|
+
}
|
|
99
|
+
return num
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Default to string
|
|
103
|
+
return input
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function saveEdit() {
|
|
107
|
+
try {
|
|
108
|
+
let newValue: unknown
|
|
109
|
+
|
|
110
|
+
if (hasOptions) {
|
|
111
|
+
// For fields with options, use selected options
|
|
112
|
+
const values = Array.from(selectedOptions)
|
|
113
|
+
if (field?.multiple) {
|
|
114
|
+
newValue = values
|
|
115
|
+
} else {
|
|
116
|
+
newValue = values[0] ?? null
|
|
117
|
+
}
|
|
118
|
+
} else {
|
|
119
|
+
// For free-form fields, parse the text input
|
|
120
|
+
newValue = parseValue(editValue)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const result = await client.responses.commit({ [field.id]: newValue })
|
|
124
|
+
|
|
125
|
+
if (result.errors.length > 0) {
|
|
126
|
+
// Show validation error
|
|
127
|
+
editError = result.errors[0].userMessage
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
isEditing = false
|
|
132
|
+
editError = ''
|
|
133
|
+
selectedOptions = new Set()
|
|
134
|
+
} catch (error) {
|
|
135
|
+
editError = error instanceof Error ? error.message : 'Failed to save'
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function handleClear() {
|
|
140
|
+
client.responses.clear(field.id)
|
|
141
|
+
}
|
|
142
|
+
</script>
|
|
143
|
+
|
|
144
|
+
<tr>
|
|
145
|
+
<td>
|
|
146
|
+
<span style="font-size: 11px; color: #666; font-family: monospace;">{field.name}</span>
|
|
147
|
+
</td>
|
|
148
|
+
<td>
|
|
149
|
+
<strong style="font-size: 13px;">{field.label}</strong>
|
|
150
|
+
</td>
|
|
151
|
+
<td>
|
|
152
|
+
{#if isEditing}
|
|
153
|
+
<div>
|
|
154
|
+
{#if hasOptions && field?.options}
|
|
155
|
+
<!-- Show options UI for fields with defined options -->
|
|
156
|
+
<div style="display: flex; flex-direction: column; gap: 0.5rem; padding: 0.5rem;">
|
|
157
|
+
{#each field.options as option (option.id)}
|
|
158
|
+
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
|
159
|
+
{#if field.multiple}
|
|
160
|
+
<!-- Multiple selection: checkboxes -->
|
|
161
|
+
<input
|
|
162
|
+
type="checkbox"
|
|
163
|
+
checked={selectedOptions.has(option.value)}
|
|
164
|
+
on:change={() => toggleOption(option.value)}
|
|
165
|
+
/>
|
|
166
|
+
{:else}
|
|
167
|
+
<!-- Single selection: radio buttons -->
|
|
168
|
+
<input
|
|
169
|
+
type="radio"
|
|
170
|
+
name="field-{field.id}"
|
|
171
|
+
checked={selectedOptions.has(option.value)}
|
|
172
|
+
on:change={() => toggleOption(option.value)}
|
|
173
|
+
/>
|
|
174
|
+
{/if}
|
|
175
|
+
<span style="font-size: 12px;">
|
|
176
|
+
{option.label}
|
|
177
|
+
{#if option.description}
|
|
178
|
+
<span style="color: #666; font-size: 11px;">({option.description})</span>
|
|
179
|
+
{/if}
|
|
180
|
+
</span>
|
|
181
|
+
</label>
|
|
182
|
+
{/each}
|
|
183
|
+
</div>
|
|
184
|
+
{:else}
|
|
185
|
+
<!-- Free-form text input for fields without options -->
|
|
186
|
+
<textarea
|
|
187
|
+
bind:value={editValue}
|
|
188
|
+
style="width: 100%; min-height: 60px; font-family: monospace; font-size: 12px; padding: 0.25rem;"
|
|
189
|
+
rows="3"
|
|
190
|
+
placeholder="Enter {field.type} value..."
|
|
191
|
+
></textarea>
|
|
192
|
+
{/if}
|
|
193
|
+
{#if editError}
|
|
194
|
+
<div style="color: red; font-size: 11px; margin-top: 0.25rem;">
|
|
195
|
+
{editError}
|
|
196
|
+
</div>
|
|
197
|
+
{/if}
|
|
198
|
+
</div>
|
|
199
|
+
{:else if response === null}
|
|
200
|
+
<span style="color: #999; font-style: italic; font-size: 12px;"> No value set </span>
|
|
201
|
+
{:else}
|
|
202
|
+
<code style="white-space: pre-wrap; word-break: break-all;">
|
|
203
|
+
{formatValue(currentValue)}
|
|
204
|
+
</code>
|
|
205
|
+
{/if}
|
|
206
|
+
</td>
|
|
207
|
+
<td>
|
|
208
|
+
<span style="font-family: monospace; color: #666; font-size: 12px;">
|
|
209
|
+
{field.type}{field.multiple ? '[]' : ''}
|
|
210
|
+
</span>
|
|
211
|
+
</td>
|
|
212
|
+
<td>
|
|
213
|
+
{#if isEditing}
|
|
214
|
+
<div style="display: flex; gap: 0.25rem; justify-content: flex-end;">
|
|
215
|
+
<button
|
|
216
|
+
on:click={saveEdit}
|
|
217
|
+
style="padding: 0.25rem 0.5rem; cursor: pointer; font-size: 11px; background: #4CAF50; color: white; border: none; border-radius: 2px;"
|
|
218
|
+
>
|
|
219
|
+
Save
|
|
220
|
+
</button>
|
|
221
|
+
<button
|
|
222
|
+
on:click={cancelEdit}
|
|
223
|
+
style="padding: 0.25rem 0.5rem; cursor: pointer; font-size: 11px; background: #f44336; color: white; border: none; border-radius: 2px;"
|
|
224
|
+
>
|
|
225
|
+
Cancel
|
|
226
|
+
</button>
|
|
227
|
+
</div>
|
|
228
|
+
{:else}
|
|
229
|
+
<div style="display: flex; gap: 0.25rem; justify-content: flex-end;">
|
|
230
|
+
<button
|
|
231
|
+
on:click={startEdit}
|
|
232
|
+
style="padding: 0.25rem 0.5rem; cursor: pointer; font-size: 11px; border: none; border-radius: 2px;"
|
|
233
|
+
>
|
|
234
|
+
{response === null ? 'Set' : 'Edit'}
|
|
235
|
+
</button>
|
|
236
|
+
{#if response !== null}
|
|
237
|
+
<button
|
|
238
|
+
on:click={handleClear}
|
|
239
|
+
style="padding: 0.25rem 0.5rem; cursor: pointer; font-size: 11px; background: #f44336; color: white; border: none; border-radius: 2px;"
|
|
240
|
+
>
|
|
241
|
+
Clear
|
|
242
|
+
</button>
|
|
243
|
+
{/if}
|
|
244
|
+
</div>
|
|
245
|
+
{/if}
|
|
246
|
+
</td>
|
|
247
|
+
</tr>
|
|
@@ -0,0 +1,12 @@
|
|
|
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>
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ResponseListItem, Field } from '@picked/client'
|
|
3
|
+
import { getPicked } from '../../../../hooks'
|
|
4
|
+
import ResponseTableRow from './ResponseTableRow.svelte'
|
|
5
|
+
|
|
6
|
+
export let responses: ResponseListItem[]
|
|
7
|
+
|
|
8
|
+
const client = getPicked()
|
|
9
|
+
|
|
10
|
+
type FilterType = 'all' | 'set' | 'unset'
|
|
11
|
+
let filter: FilterType = 'set'
|
|
12
|
+
|
|
13
|
+
$: fieldsStore = client.fields.listStore
|
|
14
|
+
$: allFields = $fieldsStore
|
|
15
|
+
|
|
16
|
+
// Create a map of fieldId -> response for quick lookup
|
|
17
|
+
$: responseMap = new Map(responses.map((r) => [r.fieldId, r]))
|
|
18
|
+
|
|
19
|
+
// Merge fields with responses, showing all fields
|
|
20
|
+
$: allFieldRows = allFields.map((field) => {
|
|
21
|
+
const response = responseMap.get(field.id)
|
|
22
|
+
return {
|
|
23
|
+
field,
|
|
24
|
+
response: response || null,
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
// Apply filter
|
|
29
|
+
$: fieldRows = allFieldRows.filter((row) => {
|
|
30
|
+
if (filter === 'set') return row.response !== null
|
|
31
|
+
if (filter === 'unset') return row.response === null
|
|
32
|
+
return true
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
function handleClearAll() {
|
|
36
|
+
client.responses.clearAll()
|
|
37
|
+
}
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<div style="display: flex; flex-direction: column;">
|
|
41
|
+
<table>
|
|
42
|
+
<thead>
|
|
43
|
+
<tr>
|
|
44
|
+
<th>ID</th>
|
|
45
|
+
<th>Field</th>
|
|
46
|
+
<th>Value</th>
|
|
47
|
+
<th>Type</th>
|
|
48
|
+
<th style="width: 120px;">Actions</th>
|
|
49
|
+
</tr>
|
|
50
|
+
</thead>
|
|
51
|
+
<tbody>
|
|
52
|
+
{#if fieldRows.length === 0}
|
|
53
|
+
<tr>
|
|
54
|
+
<td colspan="5" style="text-align: center; padding: 2rem; color: #666;">
|
|
55
|
+
No fields registered yet
|
|
56
|
+
</td>
|
|
57
|
+
</tr>
|
|
58
|
+
{:else}
|
|
59
|
+
{#each fieldRows as { field, response } (field.id)}
|
|
60
|
+
<ResponseTableRow {field} {response} />
|
|
61
|
+
{/each}
|
|
62
|
+
{/if}
|
|
63
|
+
</tbody>
|
|
64
|
+
</table>
|
|
65
|
+
<div
|
|
66
|
+
style="margin-bottom: 0.5rem; margin-top: auto; padding: 0.5rem; display: flex; justify-content: space-between; align-items: center;"
|
|
67
|
+
>
|
|
68
|
+
<div style="display: flex; gap: 0.5rem; align-items: center;">
|
|
69
|
+
<span style="font-size: 13px; color: #666;">Show:</span>
|
|
70
|
+
<label
|
|
71
|
+
style="display: flex; align-items: center; gap: 0.25rem; cursor: pointer; font-size: 13px;"
|
|
72
|
+
>
|
|
73
|
+
<input type="radio" bind:group={filter} value="all" />
|
|
74
|
+
All
|
|
75
|
+
</label>
|
|
76
|
+
<label
|
|
77
|
+
style="display: flex; align-items: center; gap: 0.25rem; cursor: pointer; font-size: 13px;"
|
|
78
|
+
>
|
|
79
|
+
<input type="radio" bind:group={filter} value="set" />
|
|
80
|
+
Set ({responses.length})
|
|
81
|
+
</label>
|
|
82
|
+
<label
|
|
83
|
+
style="display: flex; align-items: center; gap: 0.25rem; cursor: pointer; font-size: 13px;"
|
|
84
|
+
>
|
|
85
|
+
<input type="radio" bind:group={filter} value="unset" />
|
|
86
|
+
Unset ({allFields.length - responses.length})
|
|
87
|
+
</label>
|
|
88
|
+
</div>
|
|
89
|
+
{#if responses.length > 0}
|
|
90
|
+
<button
|
|
91
|
+
on:click={handleClearAll}
|
|
92
|
+
style="padding: 0.25rem 0.5rem; cursor: pointer; background: #f44336; color: white; border: none; border-radius: 2px; font-size: 11px;"
|
|
93
|
+
>
|
|
94
|
+
Clear All
|
|
95
|
+
</button>
|
|
96
|
+
{/if}
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@picked/svelte",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.38-next.2",
|
|
4
4
|
"dependencies": {
|
|
5
5
|
"d3": "^7.9.0"
|
|
6
6
|
},
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
],
|
|
76
76
|
"module": "./dist/index.js",
|
|
77
77
|
"peerDependencies": {
|
|
78
|
-
"@picked/client": "0.0.
|
|
78
|
+
"@picked/client": "0.0.38-next.2",
|
|
79
79
|
"effector": "^23.3.0",
|
|
80
80
|
"svelte": "^5.34.3"
|
|
81
81
|
},
|