Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

* Fixed an issue that prevented Shiny from serving the `font.css` file referenced in Shiny's Bootstrap CSS file. (#1342)

* Removed temporary state where a data frame renderer would try to subset to selected rows that did not exist. (#1351)

* Restored `@render.data_frame`'s (prematurely removed) input value `input.<ID>_selected_rows()`. This value is to be considered deprecated. Please use `<ID>.input_cell_selection()["rows"]` moving forward. (#1345)

### Other changes

* `Session` is now an abstract base class, and `AppSession` is a concrete subclass of it. Also, `ExpressMockSession` has been renamed `ExpressStubSession` and is a concrete subclass of `Session`. (#1331)
Expand Down
12 changes: 12 additions & 0 deletions js/data-frame/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,18 @@ const ShinyDataGrid: FC<ShinyDataGridProps<unknown>> = ({
columnFilters,
]);

// Restored for legacy purposes. Only send selected rows to Shiny when row selection is performed.
useEffect(() => {
if (!id) return;
let shinyValue: number[] | null = null;
if (rowSelectionModes.row !== SelectionModes._rowEnum.NONE) {
const rowSelectionKeys = rowSelection.keys().toList();
const rowsById = table.getSortedRowModel().rowsById;
shinyValue = rowSelectionKeys.map((key) => rowsById[key].index).sort();
}
Shiny.setInputValue!(`${id}_selected_rows`, shinyValue);
}, [id, rowSelection, rowSelectionModes, table]);

// ### End row selection ############################################################

// ### Editable cells ###############################################################
Expand Down
6 changes: 4 additions & 2 deletions shiny/render/_data_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -493,12 +493,14 @@ def _subset_data_view(selected: bool) -> pd.DataFrame:
if cell_selection is not None and cell_selection["type"] == "row":
# Use a `set` for faster lookups
selected_row_indices_set = set(cell_selection["rows"])
nrow = data.shape[0]

# Subset the data view indices to only include the selected rows
# Subset the data view indices to only include the selected rows that are in the data
data_view_indices = [
index
for index in data_view_indices
if index in selected_row_indices_set
# Make sure the index is not larger than the number of rows
if index in selected_row_indices_set and index < nrow
]

return data.iloc[data_view_indices]
Expand Down
15 changes: 7 additions & 8 deletions shiny/render/renderer/_renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,16 +88,15 @@ class Renderer(Generic[IT]):
used!)

There are two methods that must be implemented by the subclasses:
`.auto_output_ui(self, id: str)` and either `.transform(self, value: IT)` or
`.render(self)`.
`.auto_output_ui(self)` and either `.transform(self, value: IT)` or `.render(self)`.

* In Express mode, the output renderer will automatically render its UI via
`.auto_output_ui(self, id: str)`. This helper method allows App authors to skip
adding a `ui.output_*` function to their UI, making Express mode even more
concise. If more control is needed over the UI, `@ui.hold` can be used to suppress
the auto rendering of the UI. When using `@ui.hold` on a renderer, the renderer's
UI will need to be added to the app to connect the rendered output to Shiny's
reactive graph.
`.auto_output_ui(self)`. This helper method allows App authors to skip adding a
`ui.output_*` function to their UI, making Express mode even more concise. If more
control is needed over the UI, `@ui.hold` can be used to suppress the auto
rendering of the UI. When using `@ui.hold` on a renderer, the renderer's UI will
need to be added to the app to connect the rendered output to Shiny's reactive
graph.
* The `render` method is responsible for executing the value function and performing
any transformations for the output value to be JSON-serializable (`None` is a
valid value!). To avoid the boilerplate of resolving the value function and
Expand Down
8 changes: 4 additions & 4 deletions shiny/www/shared/py-shiny/data-frame/data-frame.js

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions shiny/www/shared/py-shiny/data-frame/data-frame.js.map

Large diffs are not rendered by default.

37 changes: 37 additions & 0 deletions tests/playwright/shiny/bugs/1345-render-data-frame-input/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import pandas as pd

from shiny import App, Inputs, reactive, render, req, ui

app_ui = ui.page_fluid(
ui.output_data_frame("df1"),
ui.output_text_verbatim("selected_rows", placeholder=True),
ui.output_text_verbatim("cell_selection", placeholder=True),
)


def server(input: Inputs):
df = reactive.Value(pd.DataFrame([[1, 2], [3, 4], [5, 6]], columns=["A", "B"]))

@render.data_frame
def df1():
return render.DataGrid(df(), selection_mode="rows")

@render.text
def selected_rows():
return f"Input selected rows: {input.df1_selected_rows()}"

@render.text
def cell_selection():
cell_selection = df1.input_cell_selection()
if cell_selection is None:
req(cell_selection)
raise ValueError("Cell selection is None")
if cell_selection["type"] != "row":
raise ValueError(
f"Cell selection type is not 'row': {cell_selection['type']}"
)
rows = cell_selection["rows"]
return f"Cell selection rows: {rows}"


app = App(app_ui, server)
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from __future__ import annotations

from conftest import ShinyAppProc
from controls import OutputDataFrame, OutputTextVerbatim
from playwright.sync_api import Page


def test_row_selection(page: Page, local_app: ShinyAppProc) -> None:
page.goto(local_app.url)

df = OutputDataFrame(page, "df1")
selected_rows = OutputTextVerbatim(page, "selected_rows")
cell_selection = OutputTextVerbatim(page, "cell_selection")

df.expect_n_row(3)
selected_rows.expect_value("Input selected rows: ()")
cell_selection.expect_value("Cell selection rows: ()")

df.select_rows([0, 2])

selected_rows.expect_value("Input selected rows: (0, 2)")
cell_selection.expect_value("Cell selection rows: (0, 2)")
49 changes: 49 additions & 0 deletions tests/playwright/shiny/bugs/1351-render-data-frame-selected/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import pandas as pd

from shiny import App, Inputs, reactive, render, ui

app_ui = ui.page_fluid(
ui.markdown(
"""
## Description

When you add a row, click on it and click the clear button you get:


"""
),
ui.input_action_button("add_row", "Add row"),
ui.input_action_button("clear_table", "Clear table"),
ui.output_text_verbatim("number_of_selected_rows"),
ui.output_data_frame("df1"),
)


def server(input: Inputs):
df = reactive.Value(pd.DataFrame(columns=["A", "B"]))

@render.data_frame
def df1():
return render.DataGrid(df(), selection_mode="rows")

@reactive.effect
@reactive.event(input.add_row)
def _():
old_df = df()
new_df = pd.concat( # pyright: ignore[reportUnknownMemberType]
[old_df, pd.DataFrame([[1, 2]], columns=["A", "B"])]
)
df.set(new_df)

@render.text
def number_of_selected_rows():
df_selected = df1.data_view(selected=True)
return f"Selected rows: {len(df_selected)}"

@reactive.effect
@reactive.event(input.clear_table)
def _():
df.set(pd.DataFrame(columns=["A", "B"]))


app = App(app_ui, server)
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from __future__ import annotations

from conftest import ShinyAppProc
from controls import InputActionButton, OutputDataFrame, OutputTextVerbatim
from playwright.sync_api import Page


def test_row_selection(page: Page, local_app: ShinyAppProc) -> None:
page.goto(local_app.url)

df = OutputDataFrame(page, "df1")
add_row = InputActionButton(page, "add_row")
clear_table = InputActionButton(page, "clear_table")
selected_rows = OutputTextVerbatim(page, "number_of_selected_rows")

df.expect_n_row(0)
selected_rows.expect_value("Selected rows: 0")

add_row.click()

df.expect_n_row(1)
selected_rows.expect_value("Selected rows: 0")

df.cell_locator(0, 0).click()
df.select_rows([0])

df.expect_n_row(1)
selected_rows.expect_value("Selected rows: 1")

clear_table.click()
selected_rows.expect_value("Selected rows: 0")

bad_error_lines = [line for line in local_app.stderr._lines if "INFO:" not in line]
assert len(bad_error_lines) == 0, bad_error_lines