Skip to content

Commit c684255

Browse files
committed
Fix selection clears not propagating; rebuild js-component bundle
The frontend now round-trips a cleared selection as null/None (so deselecting an amino acid or switching proteoform propagates across the component iframes). update/filter use the "key not in selection_store" convention, so drop None-valued keys before computing data, while still echoing the full state (incl. nulls) back to the frontend so it can clear those fields everywhere. - src/render/render.py: pass an active_state without None values to update_data/filter_data; keep echoing the full state in selection_store. - tests/test_selection_clear.py: pin the StateTracker round-trip invariants (cleared field echoed as None but absent for the data computation). - js-component/dist: rebuilt Vue bundle including the matching frontend fix. https://claude.ai/code/session_01TWkpASnLwnhMAQRxuYFpNs
1 parent d0028fa commit c684255

4 files changed

Lines changed: 198 additions & 117 deletions

File tree

js-component/dist/assets/index-264a76c5.js renamed to js-component/dist/assets/index-3e659711.js

Lines changed: 112 additions & 112 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

js-component/dist/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<link rel="icon" href="./favicon.ico" />
66
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
77
<title>openms-streamlit-vue-component</title>
8-
<script type="module" crossorigin src="./assets/index-264a76c5.js"></script>
8+
<script type="module" crossorigin src="./assets/index-3e659711.js"></script>
99
<link rel="stylesheet" href="./assets/index-a58b60d0.css">
1010
</head>
1111
<body>

src/render/render.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,23 @@ def render_component(
2424
# Get State
2525
state = state_tracker.getState()
2626

27+
# Cleared selections now arrive (and are stored) as `None` rather than being
28+
# dropped, so the frontend can round-trip a deselect. update/filter logic uses
29+
# the "key not in selection_store" convention, so drop None-valued keys for the
30+
# data computation while still echoing the full state (incl. nulls) back so the
31+
# frontend can clear those fields in every component.
32+
active_state = {k: v for k, v in state.items() if v is not None}
33+
2734
# Update data with current session state
28-
data = update_data(data, out_components, state, additional_data, tool)
35+
data = update_data(data, out_components, active_state, additional_data, tool)
2936

3037
# Filter data based on selection
3138
data = filter_data(
32-
data, out_components, state, additional_data, tool
39+
data, out_components, active_state, additional_data, tool
3340
)
34-
41+
3542
# Hash updated. filtered data
36-
data['hash'] = hash_complex(data)
43+
data['hash'] = hash_complex(data)
3744

3845
# Render component
3946
data['selection_store'] = state

tests/test_selection_clear.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"""
2+
Tests for the selection-clearing round-trip used by the FLASHViewer grid.
3+
4+
Each view (Sequence View, Tag Table, Protein Table, ...) is a separate Streamlit
5+
component instance with its own frontend store; they share selection state only by
6+
round-tripping through Python's StateTracker. Clearing a selection (e.g. deselecting
7+
an amino acid, or switching proteoform) must therefore propagate back to every view.
8+
9+
The frontend sends a cleared field as `null`/`None` (App.vue maps `undefined -> null`
10+
so the clear survives JSON serialization). These tests pin the two invariants the fix
11+
relies on:
12+
13+
1. A cleared field is echoed back as `None` so every component can clear it.
14+
2. render_component strips `None`-valued keys for the data computation, preserving
15+
update.py's "key not in selection_store" convention.
16+
17+
They also document the original bug: when the cleared key was *dropped* from the
18+
payload entirely, the merge-only StateTracker kept echoing the stale value.
19+
"""
20+
21+
import os
22+
import sys
23+
24+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
25+
26+
from src.render.StateTracker import StateTracker
27+
28+
29+
def _echo_with(tracker, **overrides):
30+
"""Mimic a component returning the echoed state with `overrides` applied."""
31+
state = tracker.getState() # includes counter + id, like getState() -> frontend
32+
state.update(overrides)
33+
return state
34+
35+
36+
def _active_state(state):
37+
"""The view render.py passes to update/filter: None == "not selected" == absent."""
38+
return {k: v for k, v in state.items() if v is not None}
39+
40+
41+
def test_selecting_a_value_round_trips():
42+
tracker = StateTracker()
43+
tracker.updateState(_echo_with(tracker, AApos=5))
44+
assert tracker.getState()["AApos"] == 5
45+
assert _active_state(tracker.getState())["AApos"] == 5
46+
47+
48+
def test_clearing_a_selection_round_trips_as_none():
49+
tracker = StateTracker()
50+
tracker.updateState(_echo_with(tracker, AApos=5))
51+
assert tracker.getState()["AApos"] == 5
52+
53+
# Deselect: the frontend sends AApos=None (App.vue maps undefined -> null).
54+
tracker.updateState(_echo_with(tracker, AApos=None))
55+
echoed = tracker.getState()
56+
57+
# (1) Echoed back as None so every component clears the field locally.
58+
assert echoed["AApos"] is None
59+
# (2) The data-computation view treats None as absent (not selected).
60+
assert "AApos" not in _active_state(echoed)
61+
62+
63+
def test_dropped_key_keeps_stale_value_regression():
64+
"""Pre-fix behavior: `undefined` was dropped from the payload, so the merge-only
65+
StateTracker never learned about the clear and kept echoing the stale value.
66+
This is exactly the bug the null-bridge (send None instead of dropping) fixes."""
67+
tracker = StateTracker()
68+
tracker.updateState(_echo_with(tracker, AApos=5))
69+
70+
payload = tracker.getState()
71+
payload.pop("AApos") # simulate the JSON-dropped undefined key
72+
tracker.updateState(payload)
73+
74+
assert tracker.getState()["AApos"] == 5 # stale value survives -> the original bug

0 commit comments

Comments
 (0)