Skip to content

Commit 3818ec1

Browse files
committed
Fix #3030: Selection for DataTable cleared with custom action settings
In derivedPropsHelper.ts, selected rows may be invalidated when sorting, filtering or changing pages, while using custom action settings. Invalidation happens when sorting, filtering or pagination actions are set to custom and their values change. The code does not check wether the same callback also provides a new selected_rows value. Because invalidation runs inside a setTimeout(..., 0), when a callback updates both selection and sorting, filtering or pagination, the selection briefly appears and clears, causing a visible "flicker". To fix this, before invalidating the selection, we simply have to check wether selected_rows actually changed in the current callback. The selection is only cleared if it did not change, preventing the invalidation of the sent selection.
1 parent c1a9cd9 commit 3818ec1

File tree

2 files changed

+120
-7
lines changed

2 files changed

+120
-7
lines changed

components/dash-table/src/dash-table/components/Table/derivedPropsHelper.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ export default () => {
1818
page_current,
1919
page_size
2020
]);
21+
const selectedRowsCache = memoizeOneWithFlag(
22+
selected_rows => selected_rows
23+
);
2124
const sortCache = memoizeOneWithFlag(sort => sort);
2225
const viewportCache = memoizeOneWithFlag(viewport => viewport);
2326
const viewportSelectedColumnsCache = memoizeOneWithFlag(
@@ -37,6 +40,7 @@ export default () => {
3740
page_action,
3841
page_current,
3942
page_size,
43+
selected_rows,
4044
sort_action,
4145
sort_by,
4246
viewport,
@@ -64,17 +68,19 @@ export default () => {
6468
const invalidatedFilter = filterCache(filter_query);
6569
const invalidatedPagination = paginationCache(page_current, page_size);
6670
const invalidatedSort = sortCache(sort_by);
71+
const invalidatedSelectedRows = selectedRowsCache(selected_rows);
6772

6873
const invalidateSelection =
69-
(!invalidatedFilter.cached &&
74+
invalidatedSelectedRows.cached &&
75+
((!invalidatedFilter.cached &&
7076
!invalidatedFilter.first &&
7177
filter_action.type === TableAction.Custom) ||
72-
(!invalidatedPagination.cached &&
73-
!invalidatedPagination.first &&
74-
page_action === TableAction.Custom) ||
75-
(!invalidatedSort.cached &&
76-
!invalidatedSort.first &&
77-
sort_action === TableAction.Custom);
78+
(!invalidatedPagination.cached &&
79+
!invalidatedPagination.first &&
80+
page_action === TableAction.Custom) ||
81+
(!invalidatedSort.cached &&
82+
!invalidatedSort.first &&
83+
sort_action === TableAction.Custom));
7884

7985
const newProps: Partial<SanitizedAndDerivedProps> = {};
8086

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import dash
2+
from dash.dependencies import Input, Output
3+
from dash import html
4+
from dash.dash_table import DataTable
5+
6+
import json
7+
import time
8+
import pandas as pd
9+
10+
url = "https://github.com/plotly/datasets/raw/master/" "26k-consumer-complaints.csv"
11+
rawDf = pd.read_csv(url, nrows=100)
12+
rawDf["id"] = rawDf.index + 3000
13+
df = rawDf.to_dict("records")
14+
15+
16+
def get_app():
17+
app = dash.Dash(__name__)
18+
19+
app.layout = html.Div(
20+
[
21+
DataTable(
22+
id="table",
23+
columns=[{"name": i, "id": i} for i in rawDf.columns],
24+
data=df,
25+
row_selectable=True,
26+
selected_rows=[],
27+
filter_action="custom",
28+
filter_query="",
29+
sort_action="custom",
30+
sort_by=[],
31+
page_action="custom",
32+
page_current=0,
33+
page_size=10,
34+
style_cell=dict(width=100, min_width=100, max_width=100),
35+
),
36+
html.Button("Set selected + sort_by", id="sort"),
37+
html.Button("Set selected + filter", id="filter"),
38+
html.Button("Set selected + page", id="page"),
39+
html.Div(id="selected_rows_output"),
40+
]
41+
)
42+
43+
@app.callback(
44+
Output("selected_rows_output", "children"),
45+
Input("table", "selected_rows"),
46+
)
47+
def show_selected_rows(selected_rows):
48+
return json.dumps(selected_rows) if selected_rows is not None else "None"
49+
50+
@app.callback(
51+
Output("table", "selected_rows"),
52+
Output("table", "sort_by"),
53+
Input("sort", "n_clicks"),
54+
prevent_initial_call=True,
55+
)
56+
def set_selected_and_sort(_):
57+
return [0, 1, 2], [{"column_id": rawDf.columns[0], "direction": "asc"}]
58+
59+
@app.callback(
60+
Output("table", "selected_rows", allow_duplicate=True),
61+
Output("table", "filter_query"),
62+
Input("filter", "n_clicks"),
63+
prevent_initial_call=True,
64+
)
65+
def set_selected_and_filter(_):
66+
return [0, 1, 2], "{} > 1".format(rawDf.columns[0])
67+
68+
@app.callback(
69+
Output("table", "selected_rows", allow_duplicate=True),
70+
Output("table", "page_current"),
71+
Input("page", "n_clicks"),
72+
prevent_initial_call=True,
73+
)
74+
def set_selected_and_page(_):
75+
return [0, 1, 2], 1
76+
77+
return app
78+
79+
80+
def test_tsrc001_selected_rows_persists_with_sort_by(test):
81+
test.start_server(get_app())
82+
83+
test.find_element("#sort").click()
84+
time.sleep(1)
85+
86+
assert test.find_element("#selected_rows_output").text == json.dumps([0, 1, 2])
87+
assert test.get_log_errors() == []
88+
89+
90+
def test_tsrc002_selected_rows_persists_with_filter_query(test):
91+
test.start_server(get_app())
92+
93+
test.find_element("#filter").click()
94+
time.sleep(1)
95+
96+
assert test.find_element("#selected_rows_output").text == json.dumps([0, 1, 2])
97+
assert test.get_log_errors() == []
98+
99+
100+
def test_tsrc003_selected_rows_persists_with_page_current(test):
101+
test.start_server(get_app())
102+
103+
test.find_element("#page").click()
104+
time.sleep(1)
105+
106+
assert test.find_element("#selected_rows_output").text == json.dumps([0, 1, 2])
107+
assert test.get_log_errors() == []

0 commit comments

Comments
 (0)