fix(downloads): swap st.download_button for an HTML <a download> helper
Reported symptom: only the FIRST download button in a multi-button
row pops the browser save dialog. The second and third do nothing on
click. Affects every tool page that exposes (cleaned + audit + config)
downloads.
Root cause is ``st.download_button`` itself — when several render in
the same script pass, the click-to-bytes wiring on the browser side
mis-routes and only one button's data is actually exposed. Explicit
``key`` arguments don't fix it; ``use_container_width=True`` doesn't
help either; we confirmed this in the Text Cleaner reverts.
Replace the widget with a real ``<a download="file" href="data:...">``
anchor rendered via ``st.markdown(..., unsafe_allow_html=True)``.
Bypasses Streamlit's widget machinery entirely; behaves identically to
a native browser download. Side benefit: clicking it does NOT trigger
a script rerun, so other in-flight UI state survives.
New helper ``html_download_button`` lives in
``src/gui/components/_legacy.py`` (exported from ``components``). API:
html_download_button(
label, data,
*, file_name, mime="application/octet-stream",
disabled=False, help=None, use_container_width=True,
)
Translation pattern applied across every tool page (and shared
``results_summary`` / ``config_panel`` widgets in ``_legacy.py``):
- ``st.download_button(`` -> ``html_download_button(``
- ``data=foo_bytes`` kwarg -> positional second arg
- ``key="..."`` -> dropped (helper has no widget identity)
- ``use_container_width=True`` -> dropped (default)
- ``disabled=`` and ``help=`` pass through unchanged
- Pre-computed byte buffers kept where they were
Total: 17 sites replaced (3 in Text Cleaner, 3 in Format
Standardizer, 3 in Fix Missing Values, 3 in Map Columns, 3 in
Automated Workflows, 2 in Find Duplicates page + 4 in shared
_legacy.py widgets used by Find Duplicates).
Caveat: data: URLs balloon by 33% (base64). Fine for tool output
sizes we ship; if a future result topped a few hundred MB we'd want a
Blob-URL fallback.
The marketing demo at src/gui/app_demo.py keeps its single
st.download_button — single button, no collision, no need to switch.
2008 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,7 @@ if str(_project_root) not in sys.path:
|
||||
from src.gui.components import (
|
||||
back_to_home_link,
|
||||
hide_streamlit_chrome,
|
||||
html_download_button,
|
||||
pickup_or_upload,
|
||||
require_feature_or_render_upgrade,
|
||||
)
|
||||
@@ -386,37 +387,31 @@ _pipeline_empty = current_pipeline is None or not current_pipeline.steps
|
||||
|
||||
dl_a, dl_b, dl_c = st.columns(3)
|
||||
with dl_a:
|
||||
st.download_button(
|
||||
html_download_button(
|
||||
"Download cleaned CSV",
|
||||
data=cleaned_bytes,
|
||||
cleaned_bytes,
|
||||
file_name=f"{stem}_pipeline.csv",
|
||||
mime="text/csv",
|
||||
key="pipeline_dl_cleaned",
|
||||
use_container_width=True,
|
||||
)
|
||||
with dl_b:
|
||||
st.download_button(
|
||||
html_download_button(
|
||||
"Download pipeline JSON",
|
||||
data=pipeline_bytes,
|
||||
pipeline_bytes,
|
||||
file_name="pipeline.json",
|
||||
mime="application/json",
|
||||
key="pipeline_dl_pipeline",
|
||||
disabled=_pipeline_empty,
|
||||
help=(
|
||||
"No pipeline defined."
|
||||
if _pipeline_empty
|
||||
else "Save this and pass --pipeline pipeline.json to the CLI to re-run on next week's file."
|
||||
),
|
||||
use_container_width=True,
|
||||
)
|
||||
with dl_c:
|
||||
st.download_button(
|
||||
html_download_button(
|
||||
"Download run audit",
|
||||
data=audit_bytes,
|
||||
audit_bytes,
|
||||
file_name=f"{stem}_pipeline_audit.json",
|
||||
mime="application/json",
|
||||
key="pipeline_dl_audit",
|
||||
use_container_width=True,
|
||||
)
|
||||
|
||||
st.divider()
|
||||
|
||||
Reference in New Issue
Block a user