From dd348f4176bed26776c42b9d01ca6f55c5fda7fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Fri, 17 Nov 2023 16:33:31 +0100 Subject: [PATCH 01/14] Setup selection-change model event --- panel/models/tabulator.py | 15 ++++++++++++++ panel/models/tabulator.ts | 41 +++++++++++++++++++++++++++++++++------ panel/widgets/tables.py | 2 +- 3 files changed, 51 insertions(+), 7 deletions(-) diff --git a/panel/models/tabulator.py b/panel/models/tabulator.py index 7d2d2bfc60..211f2922e1 100644 --- a/panel/models/tabulator.py +++ b/panel/models/tabulator.py @@ -52,6 +52,21 @@ def __repr__(self): f'value={self.value}, old={self.old})' ) +class SelectionEvent(ModelEvent): + + event_name = 'selection-change' + + def __init__(self, model, index, selected): + self.index = index + self.selected = selected + super().__init__(model=model) + + def __repr__(self): + return ( + f'{type(self).__name__}(index={self.index}, selected={self.selected})' + ) + + class CellClickEvent(ModelEvent): event_name = 'cell-click' diff --git a/panel/models/tabulator.ts b/panel/models/tabulator.ts index 4bc37c47ba..b10ca4fccf 100644 --- a/panel/models/tabulator.ts +++ b/panel/models/tabulator.ts @@ -43,6 +43,20 @@ export class CellClickEvent extends ModelEvent { } } +export class SelectionEvent extends ModelEvent { + constructor(readonly index: number, readonly selected: boolean) { + super() + } + + protected get event_values(): Attrs { + return {model: this.origin, index: this.index, selected: this.selected} + } + + static { + this.prototype.event_name = "selection-change" + } +} + declare const Tabulator: any; function find_group(key: any, value: string, records: any[]): any { @@ -475,7 +489,7 @@ export class DataTabulatorView extends HTMLBoxView { }, 50, false)) // Sync state with model - this.tabulator.on("rowSelectionChanged", (data: any, rows: any) => this.rowSelectionChanged(data, rows)) + this.tabulator.on("rowSelectionChanged", (data: any, rows: any, selected: any, deselected: any) => this.rowSelectionChanged(data, rows, selected, deselected)) this.tabulator.on("rowClick", (e: any, row: any) => this.rowClicked(e, row)) this.tabulator.on("cellEdited", (cell: any) => this.cellEdited(cell)) this.tabulator.on("dataFiltering", (filters: any) => { @@ -1091,7 +1105,7 @@ export class DataTabulatorView extends HTMLBoxView { return filtered } - rowSelectionChanged(data: any, _: any): void { + rowSelectionChanged(data: any, _row: any, selected: any, deselected: any): void { if ( this._selection_updating || this._initializing || @@ -1100,10 +1114,25 @@ export class DataTabulatorView extends HTMLBoxView { this.model.configuration.dataTree ) return - const indices: number[] = data.map((row: any) => row._index) - const filtered = this._filter_selected(indices) - this._selection_updating = indices.length === filtered.length - this.model.source.selected.indices = filtered + if (this.model.pagination === 'remote') { + let selected_index = selected.length ? selected[0]._row.data._index : null + let deselected_index = deselected.length ? deselected[0]._row.data._index : null + if (selected_index !== null) { + this._selection_updating = true + this.model.trigger_event(new SelectionEvent(selected_index, selected=true)) + console.log("Selected :", selected_index) + } + if (deselected_index !== null) { + this._selection_updating = true + this.model.trigger_event(new SelectionEvent(deselected_index, selected=false)) + console.log("Deselected: ", deselected_index) + } + } else { + const indices: number[] = data.map((row: any) => row._index) + const filtered = this._filter_selected(indices) + this._selection_updating = indices.length === filtered.length + this.model.source.selected.indices = filtered + } this._selection_updating = false } diff --git a/panel/widgets/tables.py b/panel/widgets/tables.py index fd0f0f6719..01a4da7477 100644 --- a/panel/widgets/tables.py +++ b/panel/widgets/tables.py @@ -1659,7 +1659,7 @@ def _get_model( child_panels, doc, root, parent, comm ) self._link_props(model, ['page', 'sorters', 'expanded', 'filters'], doc, root, comm) - self._register_events('cell-click', 'table-edit', model=model, doc=doc, comm=comm) + self._register_events('cell-click', 'table-edit', 'selection-change', model=model, doc=doc, comm=comm) return model def _get_filter_spec(self, column: TableColumn) -> Dict[str, Any]: From 5bda97fa2390f1d8130998ef7260effafb78bccf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Fri, 17 Nov 2023 16:35:47 +0100 Subject: [PATCH 02/14] Initial attempt at logic for remote multipage selection --- panel/widgets/tables.py | 56 +++++++++++++++++++++++++---------------- 1 file changed, 34 insertions(+), 22 deletions(-) diff --git a/panel/widgets/tables.py b/panel/widgets/tables.py index 01a4da7477..4346903ca7 100644 --- a/panel/widgets/tables.py +++ b/panel/widgets/tables.py @@ -44,7 +44,9 @@ from bokeh.models.sources import DataDict from pyviz_comms import Comm - from ..models.tabulator import CellClickEvent, TableEditEvent + from ..models.tabulator import ( + CellClickEvent, SelectionEvent, TableEditEvent, + ) def _convert_datetime_array_ignore_list(v): @@ -292,21 +294,21 @@ def _update_index_mapping(self): @updating def _update_cds(self, *events: param.parameterized.Event): - old_processed = self._processed + # old_processed = self._processed self._processed, data = self._get_data() self._update_index_mapping() # If there is a selection we have to compute new index - if self.selection and old_processed is not None: - indexes = list(self._processed.index) - selection = [] - for sel in self.selection: - try: - iv = old_processed.index[sel] - idx = indexes.index(iv) - selection.append(idx) - except Exception: - continue - self.selection = selection + # if self.selection and old_processed is not None: + # indexes = list(self._processed.index) + # selection = [] + # for sel in self.selection: + # try: + # iv = old_processed.index[sel] + # idx = indexes.index(iv) + # selection.append(idx) + # except Exception: + # continue + # self.selection = selection self._data = {k: _convert_datetime_array_ignore_list(v) for k, v in data.items()} msg = {'data': self._data} for ref, (m, _) in self._models.items(): @@ -1241,7 +1243,12 @@ def _cleanup(self, root: Model | None = None) -> None: p._cleanup(root) super()._cleanup(root) - def _process_event(self, event): + def _process_event(self, event) -> None: + + if event.event_name == 'selection-change': + self._update_selection(event) + return + event_col = self._renamed_cols.get(event.column, event.column) if self.pagination == 'remote': nrows = self.page_size @@ -1564,14 +1571,16 @@ def _update_selected(self, *events: param.parameterized.Event, indices=None): try: iloc = self._processed.index.get_loc(v) self._validate_iloc(v ,iloc) - indices.append(iloc) + indices.append((v, iloc)) except KeyError: continue nrows = self.page_size start = (self.page-1)*nrows end = start+nrows - kwargs['indices'] = [ind-start for ind in indices - if ind>=start and ind Dict[str, Any]: properties = super()._get_properties(doc) From 6a6d84eb8a45b8ee67a32325d0c1a413fdc6a605 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Fri, 17 Nov 2023 16:53:24 +0100 Subject: [PATCH 03/14] postUpdate on data change --- panel/models/tabulator.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/panel/models/tabulator.ts b/panel/models/tabulator.ts index b10ca4fccf..1091af8291 100644 --- a/panel/models/tabulator.ts +++ b/panel/models/tabulator.ts @@ -366,7 +366,14 @@ export class DataTabulatorView extends HTMLBoxView { this.connect(p.frozen_rows.change, () => this.setFrozen()) this.connect(p.sorters.change, () => this.setSorters()) this.connect(p.theme_classes.change, () => this.setCSSClasses(this.tabulator.element)) - this.connect(this.model.source.properties.data.change, () => this.setData()) + this.connect(this.model.source.properties.data.change, () => { + console.log("Before: ", this.model.source.selected.indices) + this._selection_updating = true + this.setData() + this._selection_updating = false + this.postUpdate() + console.log("After : ",this.model.source.selected.indices) + }) this.connect(this.model.source.streaming, () => this.addData()) this.connect(this.model.source.patching, () => { const inds = this.model.source.selected.indices From c6c6c61b999eb70516968921f4c5ac51673e965d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Fri, 17 Nov 2023 16:53:49 +0100 Subject: [PATCH 04/14] Subtract start --- panel/widgets/tables.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/panel/widgets/tables.py b/panel/widgets/tables.py index 4346903ca7..fd1e57d726 100644 --- a/panel/widgets/tables.py +++ b/panel/widgets/tables.py @@ -1578,7 +1578,7 @@ def _update_selected(self, *events: param.parameterized.Event, indices=None): start = (self.page-1)*nrows end = start+nrows p_range = self._processed.index[start:end] - kwargs['indices'] = [iloc for ind, iloc in indices + kwargs['indices'] = [iloc - start for ind, iloc in indices if ind in p_range] print(indices, kwargs['indices']) super()._update_selected(*events, **kwargs) From fbc81e8ba775007af4beead17d398538c0c6bc41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Fri, 17 Nov 2023 17:13:46 +0100 Subject: [PATCH 05/14] Fix existing tests --- panel/tests/widgets/test_tables.py | 6 ++--- panel/widgets/tables.py | 35 ++++++++++++++++++------------ 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/panel/tests/widgets/test_tables.py b/panel/tests/widgets/test_tables.py index 59c2143463..c7640519cf 100644 --- a/panel/tests/widgets/test_tables.py +++ b/panel/tests/widgets/test_tables.py @@ -1406,12 +1406,12 @@ def test_tabulator_paginated_sorted_selection(document, comm): table._process_events({'indices': [0, 1]}) assert table.selection == [4, 3] - table._process_events({'indices': [1]}) - assert table.selection == [3] + table._process_events({'indices': [2]}) + assert table.selection == [4, 3, 2] table.sorters = [{'field': 'A', 'sorter': 'number', 'dir': 'asc'}] table._process_events({'indices': [1]}) - assert table.selection == [1] + assert table.selection == [4, 3, 2, 1] def test_tabulator_stream_dataframe(document, comm): diff --git a/panel/widgets/tables.py b/panel/widgets/tables.py index fd1e57d726..f15b5538de 100644 --- a/panel/widgets/tables.py +++ b/panel/widgets/tables.py @@ -294,21 +294,21 @@ def _update_index_mapping(self): @updating def _update_cds(self, *events: param.parameterized.Event): - # old_processed = self._processed + old_processed = self._processed self._processed, data = self._get_data() self._update_index_mapping() # If there is a selection we have to compute new index - # if self.selection and old_processed is not None: - # indexes = list(self._processed.index) - # selection = [] - # for sel in self.selection: - # try: - # iv = old_processed.index[sel] - # idx = indexes.index(iv) - # selection.append(idx) - # except Exception: - # continue - # self.selection = selection + if self.selection and old_processed is not None and self.pagination != 'remote': + indexes = list(self._processed.index) + selection = [] + for sel in self.selection: + try: + iv = old_processed.index[sel] + idx = indexes.index(iv) + selection.append(idx) + except Exception: + continue + self.selection = selection self._data = {k: _convert_datetime_array_ignore_list(v) for k, v in data.items()} msg = {'data': self._data} for ref, (m, _) in self._models.items(): @@ -1605,9 +1605,16 @@ def _update_selection(self, indices: List[int] | SelectionEvent): if self.pagination != 'remote': self.selection = indices return + if isinstance(indices, list): + selected = True + else: + # Selection event + selected = indices.selected + indices = [indices.index] + nrows = self.page_size start = (self.page-1)*nrows - index = self._processed.iloc[[start+indices.index]].index + index = self._processed.iloc[[start+ind for ind in indices]].index ilocs = self.selection for v in index.values: try: @@ -1615,7 +1622,7 @@ def _update_selection(self, indices: List[int] | SelectionEvent): self._validate_iloc(v, iloc) except KeyError: continue - if indices.selected: + if selected: ilocs.append(iloc) else: ilocs.remove(iloc) From 8c33f7b569e49ee0df16970a0af68a3a8c146a5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Fri, 17 Nov 2023 18:04:35 +0100 Subject: [PATCH 06/14] Improve handling of selectable=True --- panel/tests/widgets/test_tables.py | 7 +++---- panel/widgets/tables.py | 10 ++++++---- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/panel/tests/widgets/test_tables.py b/panel/tests/widgets/test_tables.py index c7640519cf..6065deb2a1 100644 --- a/panel/tests/widgets/test_tables.py +++ b/panel/tests/widgets/test_tables.py @@ -1406,13 +1406,12 @@ def test_tabulator_paginated_sorted_selection(document, comm): table._process_events({'indices': [0, 1]}) assert table.selection == [4, 3] - table._process_events({'indices': [2]}) - assert table.selection == [4, 3, 2] + table._process_events({'indices': [1]}) + assert table.selection == [3] table.sorters = [{'field': 'A', 'sorter': 'number', 'dir': 'asc'}] table._process_events({'indices': [1]}) - assert table.selection == [4, 3, 2, 1] - + assert table.selection == [1] def test_tabulator_stream_dataframe(document, comm): df = makeMixedDataFrame() diff --git a/panel/widgets/tables.py b/panel/widgets/tables.py index f15b5538de..e8e618eaf0 100644 --- a/panel/widgets/tables.py +++ b/panel/widgets/tables.py @@ -298,7 +298,7 @@ def _update_cds(self, *events: param.parameterized.Event): self._processed, data = self._get_data() self._update_index_mapping() # If there is a selection we have to compute new index - if self.selection and old_processed is not None and self.pagination != 'remote': + if self.selection and old_processed is not None: indexes = list(self._processed.index) selection = [] for sel in self.selection: @@ -1539,6 +1539,8 @@ def _update_cds(self, *events): elif events and all(e.name in page_events for e in events) and not self.pagination: self._processed, _ = self._get_data() return + elif self.pagination == 'remote': + self._processed = None recompute = not all( e.name in ('page', 'page_size', 'pagination') for e in events ) @@ -1607,15 +1609,15 @@ def _update_selection(self, indices: List[int] | SelectionEvent): return if isinstance(indices, list): selected = True - else: - # Selection event + ilocs = [] + else: # SelectionEvent selected = indices.selected indices = [indices.index] + ilocs = self.selection nrows = self.page_size start = (self.page-1)*nrows index = self._processed.iloc[[start+ind for ind in indices]].index - ilocs = self.selection for v in index.values: try: iloc = self.value.index.get_loc(v) From c10d7bc3cba9bca9e0b43c7c263de10067a4218b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Mon, 20 Nov 2023 09:34:07 +0100 Subject: [PATCH 07/14] Remove print/console.log --- panel/models/tabulator.ts | 4 ---- panel/tests/widgets/test_tables.py | 1 + panel/widgets/tables.py | 2 -- 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/panel/models/tabulator.ts b/panel/models/tabulator.ts index 8c8e838db9..d00abec011 100644 --- a/panel/models/tabulator.ts +++ b/panel/models/tabulator.ts @@ -367,12 +367,10 @@ export class DataTabulatorView extends HTMLBoxView { this.connect(p.sorters.change, () => this.setSorters()) this.connect(p.theme_classes.change, () => this.setCSSClasses(this.tabulator.element)) this.connect(this.model.source.properties.data.change, () => { - console.log("Before: ", this.model.source.selected.indices) this._selection_updating = true this.setData() this._selection_updating = false this.postUpdate() - console.log("After : ",this.model.source.selected.indices) }) this.connect(this.model.source.streaming, () => this.addData()) this.connect(this.model.source.patching, () => { @@ -1127,12 +1125,10 @@ export class DataTabulatorView extends HTMLBoxView { if (selected_index !== null) { this._selection_updating = true this.model.trigger_event(new SelectionEvent(selected_index, selected=true)) - console.log("Selected :", selected_index) } if (deselected_index !== null) { this._selection_updating = true this.model.trigger_event(new SelectionEvent(deselected_index, selected=false)) - console.log("Deselected: ", deselected_index) } } else { const indices: number[] = data.map((row: any) => row._index) diff --git a/panel/tests/widgets/test_tables.py b/panel/tests/widgets/test_tables.py index 6065deb2a1..59c2143463 100644 --- a/panel/tests/widgets/test_tables.py +++ b/panel/tests/widgets/test_tables.py @@ -1413,6 +1413,7 @@ def test_tabulator_paginated_sorted_selection(document, comm): table._process_events({'indices': [1]}) assert table.selection == [1] + def test_tabulator_stream_dataframe(document, comm): df = makeMixedDataFrame() table = Tabulator(df) diff --git a/panel/widgets/tables.py b/panel/widgets/tables.py index e8e618eaf0..b3cae2a152 100644 --- a/panel/widgets/tables.py +++ b/panel/widgets/tables.py @@ -1295,7 +1295,6 @@ def _get_theme(self, theme, resources=None): return theme_url def _update_columns(self, event, model): - print(event.name) if event.name not in self._config_params: super()._update_columns(event, model) if (event.name in ('editors', 'formatters', 'sortable') and @@ -1582,7 +1581,6 @@ def _update_selected(self, *events: param.parameterized.Event, indices=None): p_range = self._processed.index[start:end] kwargs['indices'] = [iloc - start for ind, iloc in indices if ind in p_range] - print(indices, kwargs['indices']) super()._update_selected(*events, **kwargs) def _update_column(self, column: str, array: np.ndarray): From 869d266fbef658f34941c2e16dfa49157354236f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Mon, 20 Nov 2023 10:32:37 +0100 Subject: [PATCH 08/14] Send a list of indexes instead of a single index --- panel/models/tabulator.py | 6 +++--- panel/models/tabulator.ts | 12 ++++++------ panel/widgets/tables.py | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/panel/models/tabulator.py b/panel/models/tabulator.py index 211f2922e1..4de589d6d5 100644 --- a/panel/models/tabulator.py +++ b/panel/models/tabulator.py @@ -56,14 +56,14 @@ class SelectionEvent(ModelEvent): event_name = 'selection-change' - def __init__(self, model, index, selected): - self.index = index + def __init__(self, model, indices, selected): + self.indices = indices self.selected = selected super().__init__(model=model) def __repr__(self): return ( - f'{type(self).__name__}(index={self.index}, selected={self.selected})' + f'{type(self).__name__}(indices={self.indices}, selected={self.selected})' ) diff --git a/panel/models/tabulator.ts b/panel/models/tabulator.ts index d00abec011..809fb60a76 100644 --- a/panel/models/tabulator.ts +++ b/panel/models/tabulator.ts @@ -44,12 +44,12 @@ export class CellClickEvent extends ModelEvent { } export class SelectionEvent extends ModelEvent { - constructor(readonly index: number, readonly selected: boolean) { + constructor(readonly indices: Array, readonly selected: boolean) { super() } protected get event_values(): Attrs { - return {model: this.origin, index: this.index, selected: this.selected} + return {model: this.origin, indices: this.indices, selected: this.selected} } static { @@ -1120,13 +1120,13 @@ export class DataTabulatorView extends HTMLBoxView { ) return if (this.model.pagination === 'remote') { - let selected_index = selected.length ? selected[0]._row.data._index : null - let deselected_index = deselected.length ? deselected[0]._row.data._index : null - if (selected_index !== null) { + let selected_index = selected.map((x: any) => x._row.data._index) + let deselected_index = deselected.map((x: any) => x._row.data._index) + if (selected_index.length > 0) { this._selection_updating = true this.model.trigger_event(new SelectionEvent(selected_index, selected=true)) } - if (deselected_index !== null) { + if (deselected_index.length > 0) { this._selection_updating = true this.model.trigger_event(new SelectionEvent(deselected_index, selected=false)) } diff --git a/panel/widgets/tables.py b/panel/widgets/tables.py index b3cae2a152..a3618fb08b 100644 --- a/panel/widgets/tables.py +++ b/panel/widgets/tables.py @@ -1610,7 +1610,7 @@ def _update_selection(self, indices: List[int] | SelectionEvent): ilocs = [] else: # SelectionEvent selected = indices.selected - indices = [indices.index] + indices = indices.indices ilocs = self.selection nrows = self.page_size From 58ea9cca83ee76134eb36a3ad4ac1957415a2431 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Mon, 20 Nov 2023 10:32:49 +0100 Subject: [PATCH 09/14] First tests --- panel/tests/ui/widgets/test_tabulator.py | 47 ++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/panel/tests/ui/widgets/test_tabulator.py b/panel/tests/ui/widgets/test_tabulator.py index 731130a03f..e94df0d246 100644 --- a/panel/tests/ui/widgets/test_tabulator.py +++ b/panel/tests/ui/widgets/test_tabulator.py @@ -3277,3 +3277,50 @@ def test_tabulator_update_hidden_columns(page): (title.bounding_box()['x'] == cell.bounding_box()['x']) and (title.bounding_box()['width'] == cell.bounding_box()['width']) ), page) + + +class Test_CheckboxSelection_RemotePagination: + + def setup_method(self): + self.widget = Tabulator( + value=pd.DataFrame(np.arange(20) + 100), + disabled=True, + pagination="remote", + page_size=10, + selectable="checkbox", + header_filters=True, + ) + + def check_selected(self, page, expected, ui_count=None): + if ui_count is None: + ui_count = len(expected) + + expect(page.locator('.tabulator-selected')).to_have_count(ui_count) + wait_until(lambda: self.widget.selection == expected, page) + + def get_checkboxes(self, page): + return page.locator('input[type="checkbox"]') + + def test_full_firstpage(self, page): + serve_component(page, self.widget) + checkboxes = self.get_checkboxes(page) + + # Select all items on page + checkboxes.nth(0).click() + self.check_selected(page, list(range(10))) + + # Deselect last one + checkboxes.last.click() + self.check_selected(page, list(range(9))) + + def test_one_item_first_page(self, page): + serve_component(page, self.widget) + checkboxes = self.get_checkboxes(page) + + # Select all items on page + checkboxes.nth(1).click() + self.check_selected(page, [0]) + + # Deselect last one + checkboxes.nth(1).click() + self.check_selected(page, []) From f8f7299abd810f140533944e6d8cea68c87021ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Mon, 20 Nov 2023 12:34:35 +0100 Subject: [PATCH 10/14] Add one more test --- panel/tests/ui/widgets/test_tabulator.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/panel/tests/ui/widgets/test_tabulator.py b/panel/tests/ui/widgets/test_tabulator.py index e94df0d246..8472c749ee 100644 --- a/panel/tests/ui/widgets/test_tabulator.py +++ b/panel/tests/ui/widgets/test_tabulator.py @@ -3301,6 +3301,9 @@ def check_selected(self, page, expected, ui_count=None): def get_checkboxes(self, page): return page.locator('input[type="checkbox"]') + def goto_page(self, page, page_number): + page.locator(f'button.tabulator-page[data-page="{page_number}"]').click() + def test_full_firstpage(self, page): serve_component(page, self.widget) checkboxes = self.get_checkboxes(page) @@ -3317,10 +3320,21 @@ def test_one_item_first_page(self, page): serve_component(page, self.widget) checkboxes = self.get_checkboxes(page) - # Select all items on page checkboxes.nth(1).click() self.check_selected(page, [0]) - # Deselect last one checkboxes.nth(1).click() self.check_selected(page, []) + + def test_one_item_first_page_goto_second_page(self, page): + serve_component(page, self.widget) + checkboxes = self.get_checkboxes(page) + + checkboxes.nth(1).click() + self.check_selected(page, [0], 1) + + self.goto_page(page, 2) + self.check_selected(page, [0], 0) + + self.goto_page(page, 1) + self.check_selected(page, [0], 1) From 2c2d5a48cdef3a6da23b9f6b64e2a529e64f24d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Mon, 20 Nov 2023 12:35:19 +0100 Subject: [PATCH 11/14] rename index to indices --- panel/models/tabulator.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/panel/models/tabulator.ts b/panel/models/tabulator.ts index 809fb60a76..a2951ce254 100644 --- a/panel/models/tabulator.ts +++ b/panel/models/tabulator.ts @@ -44,7 +44,7 @@ export class CellClickEvent extends ModelEvent { } export class SelectionEvent extends ModelEvent { - constructor(readonly indices: Array, readonly selected: boolean) { + constructor(readonly indices: number[], readonly selected: boolean) { super() } @@ -1120,15 +1120,15 @@ export class DataTabulatorView extends HTMLBoxView { ) return if (this.model.pagination === 'remote') { - let selected_index = selected.map((x: any) => x._row.data._index) - let deselected_index = deselected.map((x: any) => x._row.data._index) - if (selected_index.length > 0) { + let selected_indices = selected.map((x: any) => x._row.data._index) + let deselected_indices = deselected.map((x: any) => x._row.data._index) + if (selected_indices.length > 0) { this._selection_updating = true - this.model.trigger_event(new SelectionEvent(selected_index, selected=true)) + this.model.trigger_event(new SelectionEvent(selected_indices, selected=true)) } - if (deselected_index.length > 0) { + if (deselected_indices.length > 0) { this._selection_updating = true - this.model.trigger_event(new SelectionEvent(deselected_index, selected=false)) + this.model.trigger_event(new SelectionEvent(deselected_indices, selected=false)) } } else { const indices: number[] = data.map((row: any) => row._index) From 43563175a1d6914c1926ae768d921aa3d09570fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Mon, 20 Nov 2023 14:55:44 +0100 Subject: [PATCH 12/14] Add more tests --- panel/tests/ui/widgets/test_tabulator.py | 83 ++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/panel/tests/ui/widgets/test_tabulator.py b/panel/tests/ui/widgets/test_tabulator.py index 8472c749ee..5892e04529 100644 --- a/panel/tests/ui/widgets/test_tabulator.py +++ b/panel/tests/ui/widgets/test_tabulator.py @@ -3304,6 +3304,15 @@ def get_checkboxes(self, page): def goto_page(self, page, page_number): page.locator(f'button.tabulator-page[data-page="{page_number}"]').click() + def click_sorting(self, page): + page.locator('div.tabulator-col-title').get_by_text("index").click() + page.wait_for_timeout(100) + + def set_filtering(self, page, number): + number_input = page.locator('input[type="number"]').first + number_input.fill(str(number)) + number_input.press("Enter") + def test_full_firstpage(self, page): serve_component(page, self.widget) checkboxes = self.get_checkboxes(page) @@ -3338,3 +3347,77 @@ def test_one_item_first_page_goto_second_page(self, page): self.goto_page(page, 1) self.check_selected(page, [0], 1) + + def test_one_item_both_pages_python(self, page): + serve_component(page, self.widget) + + self.widget.selection = [0, 10] + self.check_selected(page, [0, 10], 1) + + self.goto_page(page, 2) + self.check_selected(page, [0, 10], 1) + + @pytest.mark.parametrize("selection", (0, 10), ids=["page1", "page2"]) + def test_sorting(self, page, selection): + self.widget.selection = [selection] + serve_component(page, self.widget) + self.check_selected(page, [selection], int(selection == 0)) + + # First sort ascending + self.click_sorting(page) + self.check_selected(page, [selection], int(selection == 0)) + + # Then sort descending + self.click_sorting(page) + self.check_selected(page, [selection], int(selection == 10)) + + # Then back to ascending + self.click_sorting(page) + self.check_selected(page, [selection], int(selection == 0)) + + def test_sorting_all(self, page): + serve_component(page, self.widget) + checkboxes = self.get_checkboxes(page) + + # Select all items on page + checkboxes.nth(0).click() + + # First sort ascending + self.click_sorting(page) + self.check_selected(page, list(range(10)), 10) + + # Then sort descending + self.click_sorting(page) + self.check_selected(page, list(range(10)), 0) + + # Then back to ascending + self.click_sorting(page) + self.check_selected(page, list(range(10)), 10) + + @pytest.mark.parametrize("selection", (0, 10), ids=["page1", "page2"]) + def test_filtering(self, page, selection): + self.widget.selection = [selection] + serve_component(page, self.widget) + self.check_selected(page, [selection], int(selection == 0)) + + self.set_filtering(page, selection) + self.check_selected(page, [selection], 1) + + self.set_filtering(page, 1) + self.check_selected(page, [selection], 0) + + def test_filtering_all(self, page): + serve_component(page, self.widget) + checkboxes = self.get_checkboxes(page) + + # Select all items on page + checkboxes.nth(0).click() + + for n in range(10): + self.set_filtering(page, n) + self.check_selected(page, list(range(10)), 1) + + for n in range(10, 20): + self.set_filtering(page, n) + self.check_selected(page, list(range(10)), 0) + expect(page.locator('.tabulator')).to_have_count(1) From 5eb22b21d9bc139590d8414295768dfea4c860fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Mon, 20 Nov 2023 15:01:23 +0100 Subject: [PATCH 13/14] Clean up --- panel/widgets/tables.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/panel/widgets/tables.py b/panel/widgets/tables.py index a3618fb08b..e154ed7960 100644 --- a/panel/widgets/tables.py +++ b/panel/widgets/tables.py @@ -1244,7 +1244,6 @@ def _cleanup(self, root: Model | None = None) -> None: super()._cleanup(root) def _process_event(self, event) -> None: - if event.event_name == 'selection-change': self._update_selection(event) return @@ -1538,7 +1537,11 @@ def _update_cds(self, *events): elif events and all(e.name in page_events for e in events) and not self.pagination: self._processed, _ = self._get_data() return - elif self.pagination == 'remote': + elif ( + self.pagination == 'remote' + and isinstance(self.selectable, str) + and "checkbox" in self.selectable + ): self._processed = None recompute = not all( e.name in ('page', 'page_size', 'pagination') for e in events @@ -1568,11 +1571,11 @@ def _update_selected(self, *events: param.parameterized.Event, indices=None): if self.pagination == 'remote' and self.value is not None: index = self.value.iloc[self.selection].index indices = [] - for v in index.values: + for ind in index.values: try: - iloc = self._processed.index.get_loc(v) - self._validate_iloc(v ,iloc) - indices.append((v, iloc)) + iloc = self._processed.index.get_loc(ind) + self._validate_iloc(ind ,iloc) + indices.append((ind, iloc)) except KeyError: continue nrows = self.page_size From ab16e1c30e95cc99772e90347eb20d350dcb5f83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Tue, 21 Nov 2023 19:34:36 +0100 Subject: [PATCH 14/14] Add clarifying comments --- panel/models/tabulator.py | 11 +++++++++++ panel/widgets/tables.py | 2 ++ 2 files changed, 13 insertions(+) diff --git a/panel/models/tabulator.py b/panel/models/tabulator.py index 4de589d6d5..4c19ba9c8f 100644 --- a/panel/models/tabulator.py +++ b/panel/models/tabulator.py @@ -57,6 +57,17 @@ class SelectionEvent(ModelEvent): event_name = 'selection-change' def __init__(self, model, indices, selected): + """ Selection Event + + Parameters + ---------- + model : ModelEvent + An event send when a selection is changed on the frontend. + indices : list[int] + A list of changed indices selected/deselected rows. + selected : bool + If true the rows were selected, if false they were deselected. + """ self.indices = indices self.selected = selected super().__init__(model=model) diff --git a/panel/widgets/tables.py b/panel/widgets/tables.py index e154ed7960..c9b9139bc1 100644 --- a/panel/widgets/tables.py +++ b/panel/widgets/tables.py @@ -1569,6 +1569,8 @@ def _update_max_page(self): def _update_selected(self, *events: param.parameterized.Event, indices=None): kwargs = {} if self.pagination == 'remote' and self.value is not None: + # Compute integer indexes of the selected rows + # on the displayed page index = self.value.iloc[self.selection].index indices = [] for ind in index.values: