Skip to content

Commit e63a2fe

Browse files
committed
Fix locked behavior
1 parent fc3ce68 commit e63a2fe

12 files changed

Lines changed: 601 additions & 217 deletions

File tree

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ Returns a `PivotTableResult` dict containing the current `config` state.
101101
| `on_cell_click` | `Callable[[], None] \| None` | `None` | Called when a user clicks a data cell. Read the payload from `st.session_state[key]`. |
102102
| `on_config_change` | `Callable[[], None] \| None` | `None` | Called when the user changes the pivot config interactively, including toolbar and header-menu actions. |
103103
| `enable_drilldown` | `bool` | `True` | Show an inline drill-down panel with source records when a cell is clicked. |
104-
| `locked` | `bool` | `False` | Viewer mode with exploration enabled. Toolbar config controls and settings toggles are disabled, while header-menu sorting/filtering and drill-down remain available. |
104+
| `locked` | `bool` | `False` | Viewer mode with exploration enabled. Toolbar config controls are read-only, viewer-safe actions like data export and group expand/collapse remain available, and header-menu sorting/filtering/`Show Values As` plus drill-down still work. |
105105
| `export_filename` | `str \| None` | `None` | Base filename (without extension) for exported files. Date and extension are appended automatically. Defaults to `"pivot-table"`. |
106106

107107
#### Data Control
@@ -428,7 +428,7 @@ Hover over a parent column header to reveal the collapse toggle.
428428

429429
### Data Export
430430

431-
Export the pivot table as CSV, TSV, or copy to clipboard. Available via the toolbar utility menu (download icon) when `interactive=True`.
431+
Export the pivot table as CSV, TSV, or copy to clipboard. Available via the toolbar utility menu (download icon) whenever the interactive toolbar is shown, including locked viewer mode.
432432

433433
- **Format**: CSV, TSV, or Clipboard (tab-separated for pasting into spreadsheets)
434434
- **Content**: Formatted (display values with currency, percentages, etc.) or Raw (unformatted numbers)
@@ -458,7 +458,7 @@ result = st_pivot_table(
458458

459459
### Locked Mode
460460

461-
Use `locked=True` for a viewer-mode experience with exploration enabled. Toolbar config controls stay locked so end-users cannot change rows, columns, values, per-measure aggregation, or settings toggles. Reset, Swap, config import/export, and data export are hidden, while the Settings gear remains visible so users can inspect settings and use Expand/Collapse All group controls. Header-menu sorting and filtering remain available, and drill-down still works.
461+
Use `locked=True` for a viewer-mode experience with exploration enabled. Toolbar config controls stay locked so end-users cannot change rows, columns, values, per-measure aggregation, or settings toggles. Reset, Swap, and config import/export are hidden, while data export remains available and the Settings gear stays visible for read-only display status plus Expand/Collapse All group controls. Header-menu sorting, filtering, and `Show Values As` remain available, and drill-down still works.
462462

463463
```python
464464
st_pivot_table(
@@ -484,7 +484,7 @@ When `interactive=True`, hovering over the top-right of the toolbar reveals util
484484
| **Export Data** | Open the export popover (CSV / TSV / Clipboard). Use `export_filename` to customize the download filename. |
485485
| **Settings** (gear icon) | Opens a popover with display toggles: Row Totals, Column Totals, Subtotals, Repeat Labels, Sticky Headers, and Expand/Collapse All group controls |
486486

487-
In **locked mode**, Reset, Swap, config import/export, and data export are hidden. The Settings gear remains visible, settings toggles are disabled, group expand/collapse actions remain available, and header-menu sorting and filtering stay enabled.
487+
In **locked mode**, Reset, Swap, and config import/export are hidden. `Export Data` remains available as a viewer action. The Settings gear remains visible, its popover shows read-only display status plus group expand/collapse actions, and header-menu sorting, filtering, and `Show Values As` stay enabled.
488488

489489
### Non-Interactive Mode
490490

SKILL.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ Returns a `PivotTableResult` dict containing the current `config` state.
111111
| `on_cell_click` | `Callable[[], None] \| None` | `None` | Called when a user clicks a data cell. Read the payload from `st.session_state[key]`. See [Cell Click Payload](#cell-click-payload). |
112112
| `on_config_change` | `Callable[[], None] \| None` | `None` | Called when the user changes the pivot config via the toolbar. |
113113
| `enable_drilldown` | `bool` | `True` | Show an inline drill-down panel with source records when a cell is clicked. |
114-
| `locked` | `bool` | `False` | Freeze toolbar config controls (rows/columns/values/aggregation/settings). The entire utility menu is hidden. Sorting and filtering via header menus remain available. |
114+
| `locked` | `bool` | `False` | Viewer mode with exploration enabled. Toolbar config controls are read-only, while data export, group expand/collapse, and header-menu sorting/filtering/show-values-as remain available. |
115115
| `export_filename` | `str \| None` | `None` | Base filename (without extension) for exported files. Date and extension are appended automatically. Defaults to `"pivot-table"`. |
116116

117117
#### Data Control
@@ -471,7 +471,7 @@ When `interactive=True`, hovering over the top-right of the toolbar reveals util
471471
| **Export Data** | Open the export popover (CSV / TSV / Clipboard). Use `export_filename` to customize the download filename. |
472472
| **Settings** (gear icon) | Opens a popover with display toggles: Row Totals, Column Totals, Subtotals, Repeat Labels, Sticky Headers, and Expand/Collapse All group controls |
473473

474-
In **locked mode**, Reset, Swap, config import/export, and data export are hidden. The Settings gear remains visible, its toggles are disabled, and sorting/filtering via header menus remain available.
474+
In **locked mode**, Reset, Swap, and config import/export are hidden while data export remains available. The Settings gear remains visible, its popover shows read-only view status plus group expand/collapse actions, and sorting/filtering/show-values-as remain available via header menus.
475475

476476
---
477477

e2e_playwright/pivot_table.py

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,16 +125,35 @@ def handle_config():
125125
key="test_pivot_locked",
126126
rows=["Region"],
127127
columns=["Year"],
128+
values=["Revenue", "Profit"],
129+
aggregation={"Revenue": "sum", "Profit": "avg"},
130+
locked=True,
131+
interactive=True,
132+
on_config_change=lambda: None,
133+
)
134+
135+
136+
# ---------------------------------------------------------------------------
137+
# 4. Locked mode with row groups
138+
# ---------------------------------------------------------------------------
139+
st.subheader("Locked Grouped Pivot")
140+
141+
st_pivot_table(
142+
df,
143+
key="test_pivot_locked_groups",
144+
rows=["Region", "Category"],
145+
columns=["Year"],
128146
values=["Revenue"],
129147
aggregation="sum",
148+
show_subtotals=True,
130149
locked=True,
131150
interactive=True,
132151
on_config_change=lambda: None,
133152
)
134153

135154

136155
# ---------------------------------------------------------------------------
137-
# 4. Conditional formatting -- color scale + data bars
156+
# 5. Conditional formatting -- color scale + data bars
138157
# ---------------------------------------------------------------------------
139158
st.subheader("Conditional Format Pivot")
140159

@@ -165,7 +184,23 @@ def handle_config():
165184

166185

167186
# ---------------------------------------------------------------------------
168-
# 5. Number formatting
187+
# 6. Non-interactive read-only mode
188+
# ---------------------------------------------------------------------------
189+
st.subheader("Read Only Pivot")
190+
191+
st_pivot_table(
192+
df,
193+
key="test_pivot_readonly",
194+
rows=["Region"],
195+
columns=["Year"],
196+
values=["Revenue"],
197+
aggregation="sum",
198+
interactive=False,
199+
)
200+
201+
202+
# ---------------------------------------------------------------------------
203+
# 7. Number formatting
169204
# ---------------------------------------------------------------------------
170205
st.subheader("Number Format Pivot")
171206

@@ -183,7 +218,7 @@ def handle_config():
183218

184219

185220
# ---------------------------------------------------------------------------
186-
# 6. Drilldown
221+
# 8. Drilldown
187222
# ---------------------------------------------------------------------------
188223
st.subheader("Drilldown Pivot")
189224

e2e_playwright/pivot_table_test.py

Lines changed: 130 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@
3939
"test_pivot",
4040
"test_pivot_subtotals",
4141
"test_pivot_locked",
42+
"test_pivot_locked_groups",
4243
"test_pivot_cond_fmt",
44+
"test_pivot_readonly",
4345
"test_pivot_number_fmt",
4446
"test_pivot_drilldown",
4547
"test_pivot_empty",
@@ -865,7 +867,7 @@ def test_number_format_currency(page_at_app: Page):
865867

866868

867869
def test_locked_mode_toolbar_disabled(page_at_app: Page):
868-
"""In locked mode, toolbar dropdowns are disabled and config buttons hidden."""
870+
"""In locked mode, authoring controls are hidden but viewer actions remain."""
869871
page = page_at_app
870872
container = get_pivot(page, "test_pivot_locked")
871873
expect(container.get_by_test_id("pivot-table")).to_be_visible(timeout=15000)
@@ -880,6 +882,7 @@ def test_locked_mode_toolbar_disabled(page_at_app: Page):
880882

881883
expect(container.get_by_test_id("toolbar-rows-select")).to_have_count(0)
882884
expect(container.get_by_test_id("toolbar-swap")).to_have_count(0)
885+
expect(container.get_by_test_id("toolbar-export-data")).to_be_visible()
883886
expect(container.get_by_test_id("toolbar-settings")).to_be_visible()
884887

885888

@@ -903,10 +906,84 @@ def test_locked_mode_header_sort_and_filter_still_work(page_at_app: Page):
903906
checkboxes = filter_section.locator("input[type=checkbox]")
904907
assert checkboxes.count() > 0
905908

906-
menu.get_by_test_id("header-sort-key-desc").click()
909+
menu.get_by_test_id("header-sort-key-desc").evaluate("el => el.click()")
907910
expect(container.get_by_test_id("pivot-row-header").first).to_have_text("West")
908911

909912

913+
def test_locked_mode_show_values_as_still_works(page_at_app: Page):
914+
"""In locked mode, value-header display modes remain available."""
915+
page = page_at_app
916+
container = get_pivot(page, "test_pivot_locked")
917+
expect(container.get_by_test_id("pivot-table")).to_be_visible(timeout=15000)
918+
919+
trigger = container.get_by_test_id("header-menu-trigger-Revenue").first
920+
expect(trigger).to_be_visible(timeout=5000)
921+
trigger.click()
922+
923+
display_group = container.get_by_test_id("header-menu-display")
924+
expect(display_group).to_be_visible(timeout=5000)
925+
container.get_by_test_id("header-display-pct_of_total").click()
926+
927+
expect(container.get_by_test_id("pivot-data-cell").first).to_contain_text(
928+
"%", timeout=10000
929+
)
930+
931+
932+
def test_locked_mode_export_data_panel_opens(page_at_app: Page):
933+
"""In locked mode, export remains available as a viewer action."""
934+
page = page_at_app
935+
container = get_pivot(page, "test_pivot_locked")
936+
expect(container.get_by_test_id("pivot-toolbar")).to_be_visible(timeout=15000)
937+
938+
button = container.get_by_test_id("toolbar-export-data")
939+
button.scroll_into_view_if_needed()
940+
button.evaluate("el => el.click()")
941+
panel = page.get_by_test_id("toolbar-export-data-panel")
942+
expect(panel).to_be_visible(timeout=5000)
943+
expect(panel.get_by_test_id("export-format-csv")).to_be_visible()
944+
expect(panel.get_by_test_id("export-content-formatted")).to_be_visible()
945+
946+
947+
def test_locked_mode_csv_download_content(page_at_app: Page):
948+
"""Locked mode can complete a CSV export successfully."""
949+
page = page_at_app
950+
container = get_pivot(page, "test_pivot_locked")
951+
expect(container.get_by_test_id("pivot-toolbar")).to_be_visible(timeout=15000)
952+
953+
button = container.get_by_test_id("toolbar-export-data")
954+
button.scroll_into_view_if_needed()
955+
button.evaluate("el => el.click()")
956+
panel = page.get_by_test_id("toolbar-export-data-panel")
957+
expect(panel).to_be_visible(timeout=5000)
958+
959+
panel.get_by_test_id("export-format-csv").click()
960+
panel.get_by_test_id("export-content-raw").click()
961+
962+
with page.expect_download() as dl_info:
963+
panel.get_by_test_id("toolbar-export-data-action").click()
964+
965+
download = dl_info.value
966+
path = download.path()
967+
assert path is not None
968+
969+
content = Path(path).read_text()
970+
assert "Region" in content
971+
assert len(content.strip().splitlines()) > 1
972+
973+
974+
def test_readonly_mode_hides_toolbar_and_menu_actions(page_at_app: Page):
975+
"""interactive=False hides the toolbar and removes header-menu config actions."""
976+
page = page_at_app
977+
container = get_pivot(page, "test_pivot_readonly")
978+
expect(container.get_by_test_id("pivot-table")).to_be_visible(timeout=15000)
979+
980+
expect(container.get_by_test_id("pivot-toolbar")).to_have_count(0)
981+
expect(container.get_by_test_id("header-menu-trigger-Region")).to_have_count(0)
982+
983+
container.get_by_test_id("pivot-data-cell").first.click()
984+
expect(container.get_by_test_id("drilldown-panel")).to_be_visible(timeout=5000)
985+
986+
910987
# =====================================================================
911988
# 12. Empty / Edge Cases (3 tests)
912989
# =====================================================================
@@ -958,9 +1035,11 @@ def test_export_data_panel_opens(page_at_app: Page):
9581035
container = get_pivot(page, "test_pivot")
9591036
expect(container.get_by_test_id("pivot-toolbar")).to_be_visible(timeout=15000)
9601037

961-
container.get_by_test_id("toolbar-export-data").click(force=True)
1038+
button = container.get_by_test_id("toolbar-export-data")
1039+
button.scroll_into_view_if_needed()
1040+
button.evaluate("el => el.click()")
9621041

963-
panel = container.get_by_test_id("toolbar-export-data-panel")
1042+
panel = page.get_by_test_id("toolbar-export-data-panel")
9641043
expect(panel).to_be_visible(timeout=5000)
9651044

9661045
expect(panel.get_by_test_id("export-format-csv")).to_be_visible()
@@ -1414,16 +1493,60 @@ def test_action_bar_always_visible(page_at_app: Page):
14141493

14151494

14161495
def test_locked_mode_gear_visible_and_functional(page_at_app: Page):
1417-
"""In locked mode, the settings gear is visible and opens the popover."""
1496+
"""In locked mode, the settings gear opens a viewer-oriented popover."""
14181497
page = page_at_app
14191498
container = get_pivot(page, "test_pivot_locked")
14201499
expect(container.get_by_test_id("pivot-table")).to_be_visible(timeout=15000)
14211500

14221501
expect(container.get_by_test_id("toolbar-settings")).to_be_visible()
14231502

14241503
open_settings_popover(container)
1425-
expect(container.get_by_test_id("toolbar-row-totals")).to_be_visible()
1426-
expect(container.get_by_test_id("toolbar-col-totals")).to_be_visible()
1504+
expect(container.get_by_test_id("toolbar-row-totals-status")).to_be_visible()
1505+
expect(container.get_by_test_id("toolbar-col-totals-status")).to_be_visible()
1506+
1507+
1508+
def test_locked_mode_group_actions_still_work(page_at_app: Page):
1509+
"""In locked mode, settings popover group actions still collapse/expand rows."""
1510+
page = page_at_app
1511+
container = get_pivot(page, "test_pivot_locked_groups")
1512+
expect(container.get_by_test_id("pivot-table")).to_be_visible(timeout=15000)
1513+
1514+
button = container.get_by_test_id("toolbar-settings")
1515+
button.scroll_into_view_if_needed()
1516+
button.evaluate("el => el.click()")
1517+
panel = page.get_by_test_id("toolbar-settings-panel")
1518+
expect(panel).to_be_visible(timeout=5000)
1519+
panel.get_by_test_id("pivot-group-toggle-expand-all").click()
1520+
1521+
data_rows = container.get_by_test_id("pivot-data-row")
1522+
rows_expanded = data_rows.count()
1523+
assert rows_expanded > 0
1524+
1525+
expect(panel.get_by_test_id("toolbar-subtotals-status")).to_be_visible()
1526+
panel.get_by_test_id("pivot-group-toggle-collapse-all").click()
1527+
expect(data_rows).not_to_have_count(rows_expanded, timeout=10000)
1528+
rows_collapsed = data_rows.count()
1529+
assert rows_collapsed < rows_expanded
1530+
1531+
panel.get_by_test_id("pivot-group-toggle-expand-all").click()
1532+
expect(data_rows).to_have_count(rows_expanded, timeout=10000)
1533+
1534+
1535+
def test_locked_mode_inline_row_dim_toggle_still_works(page_at_app: Page):
1536+
"""Locked mode still allows inline row dimension toggles for group exploration."""
1537+
page = page_at_app
1538+
container = get_pivot(page, "test_pivot_locked_groups")
1539+
expect(container.get_by_test_id("pivot-table")).to_be_visible(timeout=15000)
1540+
1541+
rows_before = container.get_by_test_id("pivot-data-row").count()
1542+
toggle = container.get_by_test_id("pivot-dim-toggle-row-0-region")
1543+
expect(toggle).to_have_attribute("aria-expanded", "true")
1544+
1545+
toggle.click()
1546+
expect(toggle).to_have_attribute("aria-expanded", "false", timeout=10000)
1547+
1548+
rows_after = container.get_by_test_id("pivot-data-row").count()
1549+
assert rows_after < rows_before
14271550

14281551

14291552
# =====================================================================

streamlit_app.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -166,15 +166,17 @@
166166
Control which values appear via **header menu filters** or the Python API.
167167
**Locked mode** freezes the toolbar config controls so end-users cannot change
168168
rows, columns, values, or per-measure aggregation — but sorting and filtering via header
169-
menus still work. **Custom sorters** enforce a specific dimension order.
169+
menus still work, **Show Values As** remains available on value headers, and
170+
export still stays available as a viewer action. **Custom sorters** enforce a specific dimension order.
170171
171172
**Try it (left table):**
172173
- Click the **⋮** menu icon on the "Region" header → uncheck regions to filter them out.
173174
- Use the search box to find specific values quickly.
174175
175-
**Right table** is **locked** — the toolbar config controls and utility actions
176-
(reset, swap, import/export) are hidden, the **Settings** gear remains visible
177-
for inspection only, and you can still sort and filter via the column header menus.
176+
**Right table** is **locked** — authoring actions like reset, swap, and config import/export
177+
are hidden, but **Export Data** remains available, the **Settings** gear shows
178+
read-only view status, and you can still sort, filter, and change **Show Values As**
179+
from the header menus.
178180
179181
**API parameters used:** `hidden_from_aggregators`, `sorters`, `locked`
180182
"""

streamlit_pivot_table/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -497,9 +497,9 @@ def st_pivot_table(
497497
of values in the desired order.
498498
locked : bool
499499
If True, toolbar config controls are disabled. The settings gear stays
500-
visible so users can inspect settings and expand/collapse groups, but
501-
settings toggles are disabled. Header-menu sorting and filtering remain
502-
available. Defaults to False.
500+
visible so users can inspect current view status and expand/collapse
501+
groups. Data export plus header-menu sorting, filtering, and show-values-as
502+
remain available. Defaults to False.
503503
menu_limit : int or None
504504
Max items to show in the header-menu filter checklist. Defaults
505505
to 50 when None.

0 commit comments

Comments
 (0)