refactor: replace dmc.Drawer with dmc.Modal for filter selection (Task 7.4 + 7.5)
- Created 3 separate modals: Drug Selection (lg), Trust Selection (sm), Directorate Browser (xl) with centered overlay - Added filter trigger buttons to filter bar with count badges - Added "Clear All" button in filter bar for global filter reset - Per-modal clear buttons for drugs and trusts - Preserved all existing selection logic (same component IDs) - Deleted drawer.py component and callbacks (replaced by modals.py) - Updated CSS: filter-btn styles, modal chip/badge styles
This commit is contained in:
@@ -6,9 +6,9 @@ def register_callbacks(app):
|
||||
from dash_app.callbacks.filters import register_filter_callbacks
|
||||
from dash_app.callbacks.chart import register_chart_callbacks
|
||||
from dash_app.callbacks.kpi import register_kpi_callbacks
|
||||
from dash_app.callbacks.drawer import register_drawer_callbacks
|
||||
from dash_app.callbacks.modals import register_modal_callbacks
|
||||
|
||||
register_filter_callbacks(app)
|
||||
register_chart_callbacks(app)
|
||||
register_kpi_callbacks(app)
|
||||
register_drawer_callbacks(app)
|
||||
register_modal_callbacks(app)
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
"""Callbacks for drug selection: fragment matching and clear filters.
|
||||
|
||||
The open_drawer callback was removed in Task 7.3 (sidebar restructure) because
|
||||
the sidebar no longer has filter trigger items. Task 7.4 will replace the drawer
|
||||
with modals opened from the filter bar.
|
||||
"""
|
||||
from dash import Input, Output, State, ctx, no_update, ALL
|
||||
|
||||
|
||||
def register_drawer_callbacks(app):
|
||||
"""Register drug/trust selection callbacks (fragment matching + clear)."""
|
||||
|
||||
@app.callback(
|
||||
Output("all-drugs-chips", "value"),
|
||||
Output("trust-chips", "value"),
|
||||
Input({"type": "drug-fragment", "index": ALL}, "n_clicks"),
|
||||
Input("clear-drug-filters", "n_clicks"),
|
||||
State("all-drugs-chips", "value"),
|
||||
State("reference-data", "data"),
|
||||
prevent_initial_call=True,
|
||||
)
|
||||
def handle_fragment_or_clear(fragment_clicks, _clear_clicks, current_chips, ref_data):
|
||||
"""Handle drug fragment badge click (substring match) or clear all filters.
|
||||
|
||||
Fragment click: find all full drug names containing the fragment substring,
|
||||
toggle them in the chip selection. Trust chips unchanged.
|
||||
Clear click: reset both drug and trust chip selections.
|
||||
"""
|
||||
triggered = ctx.triggered_id
|
||||
|
||||
# Clear button — reset both drug and trust chips
|
||||
if triggered == "clear-drug-filters":
|
||||
return [], []
|
||||
|
||||
# Fragment badge click — triggered_id is a dict like {"type": "drug-fragment", "index": "DIR|FRAG"}
|
||||
if isinstance(triggered, dict) and triggered.get("type") == "drug-fragment":
|
||||
# Check if any fragment was actually clicked (not just initial render)
|
||||
if not any(n for n in (fragment_clicks or []) if n):
|
||||
return no_update, no_update
|
||||
|
||||
fragment_key = triggered["index"] # e.g. "CARDIOLOGY|acute coronary syndrome|RIVAROXABAN"
|
||||
fragment = fragment_key.rsplit("|", 1)[-1] if "|" in fragment_key else fragment_key
|
||||
|
||||
# Get all available drugs from reference data
|
||||
all_drugs = (ref_data or {}).get("available_drugs", [])
|
||||
|
||||
# Find drugs whose names contain this fragment (case-insensitive substring)
|
||||
matching_drugs = [
|
||||
drug for drug in all_drugs
|
||||
if fragment.upper() in drug.upper()
|
||||
]
|
||||
|
||||
if not matching_drugs:
|
||||
return no_update, no_update
|
||||
|
||||
# Toggle: if all matching drugs are already selected, deselect them;
|
||||
# otherwise, add them to selection
|
||||
current = set(current_chips or [])
|
||||
all_selected = all(d in current for d in matching_drugs)
|
||||
|
||||
if all_selected:
|
||||
updated = current - set(matching_drugs)
|
||||
else:
|
||||
updated = current | set(matching_drugs)
|
||||
|
||||
return sorted(updated), no_update
|
||||
|
||||
return no_update, no_update
|
||||
@@ -0,0 +1,136 @@
|
||||
"""Callbacks for filter selection modals: open/close, fragment matching, clear, count badges."""
|
||||
from dash import Input, Output, State, ctx, no_update, ALL
|
||||
|
||||
|
||||
def register_modal_callbacks(app):
|
||||
"""Register all modal-related callbacks."""
|
||||
|
||||
# ── Open modals from filter bar buttons ──
|
||||
|
||||
@app.callback(
|
||||
Output("drug-modal", "opened"),
|
||||
Input("open-drug-modal", "n_clicks"),
|
||||
prevent_initial_call=True,
|
||||
)
|
||||
def open_drug_modal(_n):
|
||||
return True
|
||||
|
||||
@app.callback(
|
||||
Output("trust-modal", "opened"),
|
||||
Input("open-trust-modal", "n_clicks"),
|
||||
prevent_initial_call=True,
|
||||
)
|
||||
def open_trust_modal(_n):
|
||||
return True
|
||||
|
||||
@app.callback(
|
||||
Output("directorate-modal", "opened"),
|
||||
Input("open-directorate-modal", "n_clicks"),
|
||||
prevent_initial_call=True,
|
||||
)
|
||||
def open_directorate_modal(_n):
|
||||
return True
|
||||
|
||||
# ── Fragment matching + clear ──
|
||||
|
||||
@app.callback(
|
||||
Output("all-drugs-chips", "value"),
|
||||
Output("trust-chips", "value"),
|
||||
Input({"type": "drug-fragment", "index": ALL}, "n_clicks"),
|
||||
Input("clear-drug-filters", "n_clicks"),
|
||||
Input("clear-drug-selection", "n_clicks"),
|
||||
Input("clear-trust-selection", "n_clicks"),
|
||||
Input("clear-all-filters", "n_clicks"),
|
||||
State("all-drugs-chips", "value"),
|
||||
State("trust-chips", "value"),
|
||||
State("reference-data", "data"),
|
||||
prevent_initial_call=True,
|
||||
)
|
||||
def handle_selection_actions(
|
||||
fragment_clicks, _clear_all_clicks, _clear_drug_clicks,
|
||||
_clear_trust_clicks, _clear_global_clicks,
|
||||
current_drugs, current_trusts, ref_data
|
||||
):
|
||||
"""Handle fragment clicks, per-modal clears, and global clear."""
|
||||
triggered = ctx.triggered_id
|
||||
|
||||
# Global clear (filter bar) or directorate modal clear
|
||||
if triggered in ("clear-drug-filters", "clear-all-filters"):
|
||||
return [], []
|
||||
|
||||
# Drug modal clear
|
||||
if triggered == "clear-drug-selection":
|
||||
return [], no_update
|
||||
|
||||
# Trust modal clear
|
||||
if triggered == "clear-trust-selection":
|
||||
return no_update, []
|
||||
|
||||
# Fragment badge click
|
||||
if isinstance(triggered, dict) and triggered.get("type") == "drug-fragment":
|
||||
if not any(n for n in (fragment_clicks or []) if n):
|
||||
return no_update, no_update
|
||||
|
||||
fragment_key = triggered["index"]
|
||||
fragment = fragment_key.rsplit("|", 1)[-1] if "|" in fragment_key else fragment_key
|
||||
|
||||
all_drugs = (ref_data or {}).get("available_drugs", [])
|
||||
matching_drugs = [
|
||||
drug for drug in all_drugs
|
||||
if fragment.upper() in drug.upper()
|
||||
]
|
||||
|
||||
if not matching_drugs:
|
||||
return no_update, no_update
|
||||
|
||||
current = set(current_drugs or [])
|
||||
all_selected = all(d in current for d in matching_drugs)
|
||||
|
||||
if all_selected:
|
||||
updated = current - set(matching_drugs)
|
||||
else:
|
||||
updated = current | set(matching_drugs)
|
||||
|
||||
return sorted(updated), no_update
|
||||
|
||||
return no_update, no_update
|
||||
|
||||
# ── Count badges in filter bar ──
|
||||
|
||||
@app.callback(
|
||||
Output("drug-count-badge", "children"),
|
||||
Output("drug-count-badge", "className"),
|
||||
Output("trust-count-badge", "children"),
|
||||
Output("trust-count-badge", "className"),
|
||||
Output("drug-modal-count", "children"),
|
||||
Output("trust-modal-count", "children"),
|
||||
Input("all-drugs-chips", "value"),
|
||||
Input("trust-chips", "value"),
|
||||
State("reference-data", "data"),
|
||||
)
|
||||
def update_count_badges(selected_drugs, selected_trusts, ref_data):
|
||||
"""Update selection count badges in filter bar and modal headers."""
|
||||
drug_count = len(selected_drugs or [])
|
||||
trust_count = len(selected_trusts or [])
|
||||
|
||||
all_drugs = (ref_data or {}).get("available_drugs", [])
|
||||
all_trusts = (ref_data or {}).get("available_trusts", [])
|
||||
total_drugs = len(all_drugs) if all_drugs else 42
|
||||
total_trusts = len(all_trusts) if all_trusts else 7
|
||||
|
||||
hidden = "filter-btn__badge filter-btn__badge--hidden"
|
||||
visible = "filter-btn__badge"
|
||||
|
||||
drug_badge_text = str(drug_count) if drug_count else ""
|
||||
drug_badge_class = visible if drug_count else hidden
|
||||
trust_badge_text = str(trust_count) if trust_count else ""
|
||||
trust_badge_class = visible if trust_count else hidden
|
||||
|
||||
drug_modal_text = f"{drug_count} of {total_drugs} selected"
|
||||
trust_modal_text = f"{trust_count} of {total_trusts} selected"
|
||||
|
||||
return (
|
||||
drug_badge_text, drug_badge_class,
|
||||
trust_badge_text, trust_badge_class,
|
||||
drug_modal_text, trust_modal_text,
|
||||
)
|
||||
Reference in New Issue
Block a user