@selvajs/ui 4.12.1 → 4.12.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/components/preview/InputControl.svelte +12 -7
- package/dist/compute/createSolveSession.svelte.js +4 -2
- package/dist/compute/solve-session-core.d.ts +9 -0
- package/dist/compute/solve-session-core.js +18 -0
- package/package.json +3 -3
- package/src/lib/components/preview/InputControl.svelte +12 -7
- package/src/lib/compute/createSolveSession.svelte.ts +4 -1
- package/src/lib/compute/createSolveSession.test.ts +15 -0
- package/src/lib/compute/solve-session-core.ts +20 -0
|
@@ -115,6 +115,11 @@
|
|
|
115
115
|
// change (the user didn't pick the new option), so force a solve — otherwise manual-solve
|
|
116
116
|
// schemas would keep the prior output on screen, making it look like the auto-picked option
|
|
117
117
|
// produced it.
|
|
118
|
+
// Distinguishes "never selected" from "user deliberately cleared": the empty→
|
|
119
|
+
// first-option fallback below must not fight a real user action (e.g. unchecking
|
|
120
|
+
// every checklist entry), only fill the initial void.
|
|
121
|
+
let userTouched = false;
|
|
122
|
+
|
|
118
123
|
$effect(() => {
|
|
119
124
|
if (!isDynamicValueListWidget(item) || !dynamicListHasOptions) return;
|
|
120
125
|
const validValues = new Set(Object.values(dynamicListOptions));
|
|
@@ -128,22 +133,22 @@
|
|
|
128
133
|
// result then gets replayed by the solve caches.
|
|
129
134
|
if (Array.isArray(value)) {
|
|
130
135
|
const pruned = value.filter((v) => typeof v === 'string' && validValues.has(v));
|
|
131
|
-
if (
|
|
132
|
-
// Fall back to first option when the checklist is
|
|
133
|
-
//
|
|
136
|
+
if (pruned.length !== value.length || (value.length === 0 && !userTouched)) {
|
|
137
|
+
// Fall back to first option when the checklist is fully pruned or was
|
|
138
|
+
// never selected; a user-cleared checklist stays empty.
|
|
134
139
|
onChange(item.paramId, pruned.length > 0 ? pruned : [firstOption], true);
|
|
135
140
|
}
|
|
136
141
|
} else if (
|
|
137
|
-
value
|
|
138
|
-
value === ''
|
|
139
|
-
(typeof value === 'string' && !validValues.has(value))
|
|
142
|
+
(typeof value === 'string' && value && !validValues.has(value)) ||
|
|
143
|
+
((value == null || value === '') && !userTouched)
|
|
140
144
|
) {
|
|
141
|
-
// Stale or never-selected
|
|
145
|
+
// Stale single value, or never-selected — fall back to the first option.
|
|
142
146
|
onChange(item.paramId, firstOption, true);
|
|
143
147
|
}
|
|
144
148
|
});
|
|
145
149
|
|
|
146
150
|
function commit(newValue: SupportedTypes) {
|
|
151
|
+
userTouched = true;
|
|
147
152
|
value = newValue;
|
|
148
153
|
onChange(item.paramId, newValue);
|
|
149
154
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// SolveDriver. A completed solve re-enters via report().
|
|
5
5
|
import { readExternalValue } from '../external/storage';
|
|
6
6
|
import { createComputeThrottle } from './computeThrottle.svelte';
|
|
7
|
-
import { buildInitialValues, makeInitialFlags, applyValueChange, applySolveResult } from './solve-session-core';
|
|
7
|
+
import { buildInitialValues, makeInitialFlags, applyValueChange, applySolveResult, pickInputValues } from './solve-session-core';
|
|
8
8
|
export function createSolveSession(args) {
|
|
9
9
|
let currentSchema = args.schema;
|
|
10
10
|
const flags = makeInitialFlags(currentSchema?.instanceSolve);
|
|
@@ -19,7 +19,9 @@ export function createSolveSession(args) {
|
|
|
19
19
|
hasNeverSolved: flags.hasNeverSolved
|
|
20
20
|
});
|
|
21
21
|
function dispatch() {
|
|
22
|
-
|
|
22
|
+
// Input values only: outputs merged into state.values (for widgets that read
|
|
23
|
+
// them, e.g. dynamic value lists) must not be echoed back to the transport.
|
|
24
|
+
args.driver.solve(pickInputValues(currentSchema, $state.snapshot(state.values)));
|
|
23
25
|
}
|
|
24
26
|
return {
|
|
25
27
|
get values() {
|
|
@@ -38,6 +38,15 @@ export declare function applyValueChange(state: SolveSessionState, id: string, v
|
|
|
38
38
|
state: SolveSessionState;
|
|
39
39
|
shouldSolve: boolean;
|
|
40
40
|
};
|
|
41
|
+
/**
|
|
42
|
+
* Projects the session's live values down to solve INPUTS. Solve outputs are merged
|
|
43
|
+
* into the same values map after each solve (applySolveResult) so widgets like
|
|
44
|
+
* dynamic value lists can read them — but they are not solve inputs, and echoing
|
|
45
|
+
* them back to the driver re-uploads potentially MB-sized payloads (a measured
|
|
46
|
+
* 6.4 MB options list) that no backend reads. Every transport gets this projection
|
|
47
|
+
* for free by going through the session's dispatch.
|
|
48
|
+
*/
|
|
49
|
+
export declare function pickInputValues(schema: UISchema | undefined, values: Record<string, unknown>): Record<string, unknown>;
|
|
41
50
|
/**
|
|
42
51
|
* Merges a reported solve result into the state and clears the post-solve lifecycle
|
|
43
52
|
* flags. Missing result arrays are treated as empty.
|
|
@@ -52,6 +52,24 @@ export function applyValueChange(state, id, value, instanceSolve) {
|
|
|
52
52
|
}
|
|
53
53
|
return { state, shouldSolve: true };
|
|
54
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Projects the session's live values down to solve INPUTS. Solve outputs are merged
|
|
57
|
+
* into the same values map after each solve (applySolveResult) so widgets like
|
|
58
|
+
* dynamic value lists can read them — but they are not solve inputs, and echoing
|
|
59
|
+
* them back to the driver re-uploads potentially MB-sized payloads (a measured
|
|
60
|
+
* 6.4 MB options list) that no backend reads. Every transport gets this projection
|
|
61
|
+
* for free by going through the session's dispatch.
|
|
62
|
+
*/
|
|
63
|
+
export function pickInputValues(schema, values) {
|
|
64
|
+
if (!schema?.inputs)
|
|
65
|
+
return values;
|
|
66
|
+
const picked = {};
|
|
67
|
+
for (const input of schema.inputs) {
|
|
68
|
+
if (input.id in values)
|
|
69
|
+
picked[input.id] = values[input.id];
|
|
70
|
+
}
|
|
71
|
+
return picked;
|
|
72
|
+
}
|
|
55
73
|
/**
|
|
56
74
|
* Merges a reported solve result into the state and clears the post-solve lifecycle
|
|
57
75
|
* flags. Missing result arrays are treated as empty.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@selvajs/ui",
|
|
3
|
-
"version": "4.12.
|
|
3
|
+
"version": "4.12.2",
|
|
4
4
|
"description": "Shared UI components and utilities for Selva applications",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -72,8 +72,8 @@
|
|
|
72
72
|
"svelte": "5.55.5",
|
|
73
73
|
"tailwind-variants": "^3.2.2",
|
|
74
74
|
"vitest": "^3.2.6",
|
|
75
|
-
"@selvajs/
|
|
76
|
-
"@selvajs/
|
|
75
|
+
"@selvajs/config": "0.0.2",
|
|
76
|
+
"@selvajs/schemas": "4.6.1"
|
|
77
77
|
},
|
|
78
78
|
"scripts": {
|
|
79
79
|
"dev": "vite dev",
|
|
@@ -115,6 +115,11 @@
|
|
|
115
115
|
// change (the user didn't pick the new option), so force a solve — otherwise manual-solve
|
|
116
116
|
// schemas would keep the prior output on screen, making it look like the auto-picked option
|
|
117
117
|
// produced it.
|
|
118
|
+
// Distinguishes "never selected" from "user deliberately cleared": the empty→
|
|
119
|
+
// first-option fallback below must not fight a real user action (e.g. unchecking
|
|
120
|
+
// every checklist entry), only fill the initial void.
|
|
121
|
+
let userTouched = false;
|
|
122
|
+
|
|
118
123
|
$effect(() => {
|
|
119
124
|
if (!isDynamicValueListWidget(item) || !dynamicListHasOptions) return;
|
|
120
125
|
const validValues = new Set(Object.values(dynamicListOptions));
|
|
@@ -128,22 +133,22 @@
|
|
|
128
133
|
// result then gets replayed by the solve caches.
|
|
129
134
|
if (Array.isArray(value)) {
|
|
130
135
|
const pruned = value.filter((v) => typeof v === 'string' && validValues.has(v));
|
|
131
|
-
if (
|
|
132
|
-
// Fall back to first option when the checklist is
|
|
133
|
-
//
|
|
136
|
+
if (pruned.length !== value.length || (value.length === 0 && !userTouched)) {
|
|
137
|
+
// Fall back to first option when the checklist is fully pruned or was
|
|
138
|
+
// never selected; a user-cleared checklist stays empty.
|
|
134
139
|
onChange(item.paramId, pruned.length > 0 ? pruned : [firstOption], true);
|
|
135
140
|
}
|
|
136
141
|
} else if (
|
|
137
|
-
value
|
|
138
|
-
value === ''
|
|
139
|
-
(typeof value === 'string' && !validValues.has(value))
|
|
142
|
+
(typeof value === 'string' && value && !validValues.has(value)) ||
|
|
143
|
+
((value == null || value === '') && !userTouched)
|
|
140
144
|
) {
|
|
141
|
-
// Stale or never-selected
|
|
145
|
+
// Stale single value, or never-selected — fall back to the first option.
|
|
142
146
|
onChange(item.paramId, firstOption, true);
|
|
143
147
|
}
|
|
144
148
|
});
|
|
145
149
|
|
|
146
150
|
function commit(newValue: SupportedTypes) {
|
|
151
|
+
userTouched = true;
|
|
147
152
|
value = newValue;
|
|
148
153
|
onChange(item.paramId, newValue);
|
|
149
154
|
}
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
makeInitialFlags,
|
|
13
13
|
applyValueChange,
|
|
14
14
|
applySolveResult,
|
|
15
|
+
pickInputValues,
|
|
15
16
|
type SolveSessionState
|
|
16
17
|
} from './solve-session-core';
|
|
17
18
|
|
|
@@ -75,7 +76,9 @@ export function createSolveSession(args: SolveSessionArgs): SolveSession {
|
|
|
75
76
|
});
|
|
76
77
|
|
|
77
78
|
function dispatch() {
|
|
78
|
-
|
|
79
|
+
// Input values only: outputs merged into state.values (for widgets that read
|
|
80
|
+
// them, e.g. dynamic value lists) must not be echoed back to the transport.
|
|
81
|
+
args.driver.solve(pickInputValues(currentSchema, $state.snapshot(state.values)));
|
|
79
82
|
}
|
|
80
83
|
|
|
81
84
|
return {
|
|
@@ -67,4 +67,19 @@ describe('createSolveSession.setValue', () => {
|
|
|
67
67
|
session.setValue('a', 'y', true);
|
|
68
68
|
expect(driver.solves.length).toBe(1);
|
|
69
69
|
});
|
|
70
|
+
|
|
71
|
+
it('never echoes output-keyed values back to the driver', () => {
|
|
72
|
+
const driver = recordingDriver();
|
|
73
|
+
const session = createSolveSession({ schema: schema(true), scopeKey: 's', driver });
|
|
74
|
+
// A solve result merges outputs into the session's values map (how widgets
|
|
75
|
+
// like dynamic value lists read them) — e.g. a multi-MB options payload.
|
|
76
|
+
session.report({ outputs: { out: { options: { huge: 'payload' } } } });
|
|
77
|
+
session.setValue('a', 'y');
|
|
78
|
+
expect(driver.solves.length).toBe(1);
|
|
79
|
+
expect(driver.solves[0].a).toBe('y');
|
|
80
|
+
// The output entry lives in session.values for widgets…
|
|
81
|
+
expect(session.values.out).toBeDefined();
|
|
82
|
+
// …but must not travel back through the transport.
|
|
83
|
+
expect(driver.solves[0]).not.toHaveProperty('out');
|
|
84
|
+
});
|
|
70
85
|
});
|
|
@@ -85,6 +85,26 @@ export function applyValueChange(
|
|
|
85
85
|
return { state, shouldSolve: true };
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Projects the session's live values down to solve INPUTS. Solve outputs are merged
|
|
90
|
+
* into the same values map after each solve (applySolveResult) so widgets like
|
|
91
|
+
* dynamic value lists can read them — but they are not solve inputs, and echoing
|
|
92
|
+
* them back to the driver re-uploads potentially MB-sized payloads (a measured
|
|
93
|
+
* 6.4 MB options list) that no backend reads. Every transport gets this projection
|
|
94
|
+
* for free by going through the session's dispatch.
|
|
95
|
+
*/
|
|
96
|
+
export function pickInputValues(
|
|
97
|
+
schema: UISchema | undefined,
|
|
98
|
+
values: Record<string, unknown>
|
|
99
|
+
): Record<string, unknown> {
|
|
100
|
+
if (!schema?.inputs) return values;
|
|
101
|
+
const picked: Record<string, unknown> = {};
|
|
102
|
+
for (const input of schema.inputs) {
|
|
103
|
+
if (input.id in values) picked[input.id] = values[input.id];
|
|
104
|
+
}
|
|
105
|
+
return picked;
|
|
106
|
+
}
|
|
107
|
+
|
|
88
108
|
/**
|
|
89
109
|
* Merges a reported solve result into the state and clears the post-solve lifecycle
|
|
90
110
|
* flags. Missing result arrays are treated as empty.
|