@selvajs/ui 4.12.3 → 4.12.4
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.
|
@@ -110,82 +110,34 @@
|
|
|
110
110
|
);
|
|
111
111
|
|
|
112
112
|
// When a dynamic value list recomputes, a previously-selected value may no longer be an
|
|
113
|
-
// available option. Prune the stale selection so the control shows a valid option
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
// Loop breaker for the system fallback. A definition whose computed options
|
|
124
|
-
// DEPEND on the selection can oscillate: auto-pick A → solve → new options
|
|
125
|
-
// exclude A → auto-pick B → solve → … Each cycle force-solves and re-renders
|
|
126
|
-
// the option list, which can run a tab out of memory. Bound consecutive
|
|
127
|
-
// system-initiated picks; any real user commit resets the budget.
|
|
128
|
-
const MAX_CONSECUTIVE_AUTO_PICKS = 3;
|
|
129
|
-
let autoPickCount = 0;
|
|
130
|
-
|
|
113
|
+
// available option. Prune the stale selection so the control shows a valid option instead
|
|
114
|
+
// of rendering the orphaned raw value as its own label. This is a system-initiated change
|
|
115
|
+
// (the user didn't pick the new option), so force a solve — otherwise manual-solve schemas
|
|
116
|
+
// would keep the prior output on screen, making it look like the auto-picked option produced it.
|
|
117
|
+
//
|
|
118
|
+
// INVARIANT: a dynamic value list must never dispatch an empty/null value to solve. There
|
|
119
|
+
// is always at least one option, and an empty selection reaches the definition as null/""
|
|
120
|
+
// — which throws NREs in downstream geometry components (e.g. Bounding Rectangle) and nulls
|
|
121
|
+
// every output beyond them. So every terminal state below resolves to a valid option; there
|
|
122
|
+
// is deliberately no "user cleared it, stay empty" path.
|
|
131
123
|
$effect(() => {
|
|
132
124
|
if (!isDynamicValueListWidget(item) || !dynamicListHasOptions) return;
|
|
133
125
|
const validValues = new Set(Object.values(dynamicListOptions));
|
|
134
126
|
const firstOption = Object.values(dynamicListOptions)[0];
|
|
135
|
-
// Selection is valid → stable state reached; refill the auto-pick budget.
|
|
136
|
-
const isValid = Array.isArray(value)
|
|
137
|
-
? value.length > 0 && value.every((v) => typeof v === 'string' && validValues.has(v))
|
|
138
|
-
: typeof value === 'string' && value !== '' && validValues.has(value);
|
|
139
|
-
if (isValid) {
|
|
140
|
-
autoPickCount = 0;
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
if (autoPickCount >= MAX_CONSECUTIVE_AUTO_PICKS) {
|
|
144
|
-
console.warn(
|
|
145
|
-
`[InputControl] dynamic value list "${item.paramId}": options keep invalidating the ` +
|
|
146
|
-
`auto-picked selection (${autoPickCount} consecutive system picks) — the definition's ` +
|
|
147
|
-
`options likely depend on the selection. Stopping auto-reconciliation to avoid a solve loop.`
|
|
148
|
-
);
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
// Route through onChange (not commit) — value is a one-way prop here, so writing it
|
|
152
|
-
// directly from an effect trips Svelte's binding-ownership check.
|
|
153
|
-
// A NEVER-made selection (empty string/array, e.g. no default on a fresh page)
|
|
154
|
-
// gets the same first-option fallback as a stale one: an empty selection solves
|
|
155
|
-
// as an empty string, which cascades through the definition as null-data errors
|
|
156
|
-
// ("File not found: .dmf", Text→Number conversion failures, …) and the empty
|
|
157
|
-
// result then gets replayed by the solve caches.
|
|
158
|
-
// Diagnostic: each system pick logs itself — a runaway sequence of these lines
|
|
159
|
-
// (pick #1, #2, #3 + the loop-breaker warning) is the solve-loop signature.
|
|
160
|
-
const logAutoPick = (picked: string) =>
|
|
161
|
-
console.info(
|
|
162
|
-
`[DVL] auto-pick #${autoPickCount} on "${item.paramId}": "${picked.slice(0, 60)}" ` +
|
|
163
|
-
`(was ${JSON.stringify(Array.isArray(value) ? value.slice(0, 3) : value)?.slice(0, 80)})`
|
|
164
|
-
);
|
|
165
127
|
if (Array.isArray(value)) {
|
|
166
128
|
const pruned = value.filter((v) => typeof v === 'string' && validValues.has(v));
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
autoPickCount++;
|
|
171
|
-
logAutoPick(pruned.length > 0 ? String(pruned[0]) : firstOption);
|
|
129
|
+
// Empty (never selected or fully pruned) always falls back to the first option —
|
|
130
|
+
// a checklist that solves empty produces the same null cascade as a single value.
|
|
131
|
+
if (pruned.length !== value.length || value.length === 0) {
|
|
172
132
|
onChange(item.paramId, pruned.length > 0 ? pruned : [firstOption], true);
|
|
173
133
|
}
|
|
174
|
-
} else if (
|
|
175
|
-
|
|
176
|
-
((value == null || value === '') && !userTouched)
|
|
177
|
-
) {
|
|
178
|
-
// Stale single value, or never-selected — fall back to the first option.
|
|
179
|
-
autoPickCount++;
|
|
180
|
-
logAutoPick(firstOption);
|
|
134
|
+
} else if (typeof value !== 'string' || value === '' || !validValues.has(value)) {
|
|
135
|
+
// Never-selected or stale single value — fall back to the first option.
|
|
181
136
|
onChange(item.paramId, firstOption, true);
|
|
182
137
|
}
|
|
183
138
|
});
|
|
184
139
|
|
|
185
140
|
function commit(newValue: SupportedTypes) {
|
|
186
|
-
userTouched = true;
|
|
187
|
-
// A real user pick re-arms the system fallback (see autoPickCount above).
|
|
188
|
-
autoPickCount = 0;
|
|
189
141
|
value = newValue;
|
|
190
142
|
onChange(item.paramId, newValue);
|
|
191
143
|
}
|
package/package.json
CHANGED
|
@@ -110,82 +110,34 @@
|
|
|
110
110
|
);
|
|
111
111
|
|
|
112
112
|
// When a dynamic value list recomputes, a previously-selected value may no longer be an
|
|
113
|
-
// available option. Prune the stale selection so the control shows a valid option
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
// Loop breaker for the system fallback. A definition whose computed options
|
|
124
|
-
// DEPEND on the selection can oscillate: auto-pick A → solve → new options
|
|
125
|
-
// exclude A → auto-pick B → solve → … Each cycle force-solves and re-renders
|
|
126
|
-
// the option list, which can run a tab out of memory. Bound consecutive
|
|
127
|
-
// system-initiated picks; any real user commit resets the budget.
|
|
128
|
-
const MAX_CONSECUTIVE_AUTO_PICKS = 3;
|
|
129
|
-
let autoPickCount = 0;
|
|
130
|
-
|
|
113
|
+
// available option. Prune the stale selection so the control shows a valid option instead
|
|
114
|
+
// of rendering the orphaned raw value as its own label. This is a system-initiated change
|
|
115
|
+
// (the user didn't pick the new option), so force a solve — otherwise manual-solve schemas
|
|
116
|
+
// would keep the prior output on screen, making it look like the auto-picked option produced it.
|
|
117
|
+
//
|
|
118
|
+
// INVARIANT: a dynamic value list must never dispatch an empty/null value to solve. There
|
|
119
|
+
// is always at least one option, and an empty selection reaches the definition as null/""
|
|
120
|
+
// — which throws NREs in downstream geometry components (e.g. Bounding Rectangle) and nulls
|
|
121
|
+
// every output beyond them. So every terminal state below resolves to a valid option; there
|
|
122
|
+
// is deliberately no "user cleared it, stay empty" path.
|
|
131
123
|
$effect(() => {
|
|
132
124
|
if (!isDynamicValueListWidget(item) || !dynamicListHasOptions) return;
|
|
133
125
|
const validValues = new Set(Object.values(dynamicListOptions));
|
|
134
126
|
const firstOption = Object.values(dynamicListOptions)[0];
|
|
135
|
-
// Selection is valid → stable state reached; refill the auto-pick budget.
|
|
136
|
-
const isValid = Array.isArray(value)
|
|
137
|
-
? value.length > 0 && value.every((v) => typeof v === 'string' && validValues.has(v))
|
|
138
|
-
: typeof value === 'string' && value !== '' && validValues.has(value);
|
|
139
|
-
if (isValid) {
|
|
140
|
-
autoPickCount = 0;
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
if (autoPickCount >= MAX_CONSECUTIVE_AUTO_PICKS) {
|
|
144
|
-
console.warn(
|
|
145
|
-
`[InputControl] dynamic value list "${item.paramId}": options keep invalidating the ` +
|
|
146
|
-
`auto-picked selection (${autoPickCount} consecutive system picks) — the definition's ` +
|
|
147
|
-
`options likely depend on the selection. Stopping auto-reconciliation to avoid a solve loop.`
|
|
148
|
-
);
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
// Route through onChange (not commit) — value is a one-way prop here, so writing it
|
|
152
|
-
// directly from an effect trips Svelte's binding-ownership check.
|
|
153
|
-
// A NEVER-made selection (empty string/array, e.g. no default on a fresh page)
|
|
154
|
-
// gets the same first-option fallback as a stale one: an empty selection solves
|
|
155
|
-
// as an empty string, which cascades through the definition as null-data errors
|
|
156
|
-
// ("File not found: .dmf", Text→Number conversion failures, …) and the empty
|
|
157
|
-
// result then gets replayed by the solve caches.
|
|
158
|
-
// Diagnostic: each system pick logs itself — a runaway sequence of these lines
|
|
159
|
-
// (pick #1, #2, #3 + the loop-breaker warning) is the solve-loop signature.
|
|
160
|
-
const logAutoPick = (picked: string) =>
|
|
161
|
-
console.info(
|
|
162
|
-
`[DVL] auto-pick #${autoPickCount} on "${item.paramId}": "${picked.slice(0, 60)}" ` +
|
|
163
|
-
`(was ${JSON.stringify(Array.isArray(value) ? value.slice(0, 3) : value)?.slice(0, 80)})`
|
|
164
|
-
);
|
|
165
127
|
if (Array.isArray(value)) {
|
|
166
128
|
const pruned = value.filter((v) => typeof v === 'string' && validValues.has(v));
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
autoPickCount++;
|
|
171
|
-
logAutoPick(pruned.length > 0 ? String(pruned[0]) : firstOption);
|
|
129
|
+
// Empty (never selected or fully pruned) always falls back to the first option —
|
|
130
|
+
// a checklist that solves empty produces the same null cascade as a single value.
|
|
131
|
+
if (pruned.length !== value.length || value.length === 0) {
|
|
172
132
|
onChange(item.paramId, pruned.length > 0 ? pruned : [firstOption], true);
|
|
173
133
|
}
|
|
174
|
-
} else if (
|
|
175
|
-
|
|
176
|
-
((value == null || value === '') && !userTouched)
|
|
177
|
-
) {
|
|
178
|
-
// Stale single value, or never-selected — fall back to the first option.
|
|
179
|
-
autoPickCount++;
|
|
180
|
-
logAutoPick(firstOption);
|
|
134
|
+
} else if (typeof value !== 'string' || value === '' || !validValues.has(value)) {
|
|
135
|
+
// Never-selected or stale single value — fall back to the first option.
|
|
181
136
|
onChange(item.paramId, firstOption, true);
|
|
182
137
|
}
|
|
183
138
|
});
|
|
184
139
|
|
|
185
140
|
function commit(newValue: SupportedTypes) {
|
|
186
|
-
userTouched = true;
|
|
187
|
-
// A real user pick re-arms the system fallback (see autoPickCount above).
|
|
188
|
-
autoPickCount = 0;
|
|
189
141
|
value = newValue;
|
|
190
142
|
onChange(item.paramId, newValue);
|
|
191
143
|
}
|