diff --git a/vaadin-grid-flow-parent/vaadin-grid-flow-integration-tests/src/main/java/com/vaadin/flow/component/grid/it/ConditionalSelectionPage.java b/vaadin-grid-flow-parent/vaadin-grid-flow-integration-tests/src/main/java/com/vaadin/flow/component/grid/it/ConditionalSelectionPage.java new file mode 100644 index 00000000000..36c5d57fb78 --- /dev/null +++ b/vaadin-grid-flow-parent/vaadin-grid-flow-integration-tests/src/main/java/com/vaadin/flow/component/grid/it/ConditionalSelectionPage.java @@ -0,0 +1,78 @@ +/* + * Copyright 2000-2024 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.flow.component.grid.it; + +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.NativeButton; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.data.selection.SelectionEvent; +import com.vaadin.flow.router.Route; + +@Route("vaadin-grid/conditional-selection") +public class ConditionalSelectionPage extends Div { + private final Span selectedItems; + + public ConditionalSelectionPage() { + Grid grid = new Grid<>(); + grid.setItems(IntStream.range(0, 10).boxed().toList()); + grid.addColumn(i -> i).setHeader("Item"); + + selectedItems = new Span(); + selectedItems.setId("selected-items"); + + NativeButton enableSingleSelect = new NativeButton( + "Enable single selection", e -> { + grid.setSelectionMode(Grid.SelectionMode.SINGLE); + grid.addSelectionListener(this::updateSelection); + }); + enableSingleSelect.setId("enable-single-selection"); + + NativeButton enableMultiSelect = new NativeButton( + "Enable multi selection", e -> { + grid.setSelectionMode(Grid.SelectionMode.MULTI); + grid.addSelectionListener(this::updateSelection); + }); + enableMultiSelect.setId("enable-multi-selection"); + + NativeButton disableSelectionFirstFive = new NativeButton( + "Disable selection for first five items", e -> { + grid.setItemSelectableProvider(item -> item >= 5); + }); + disableSelectionFirstFive.setId("disable-selection-first-five"); + + NativeButton allowSelectionFirstFive = new NativeButton( + "Allow selection for first five items", e -> { + grid.setItemSelectableProvider(item -> item < 5); + }); + allowSelectionFirstFive.setId("allow-selection-first-five"); + + add(grid); + add(new Div(enableSingleSelect, enableMultiSelect)); + add(new Div(disableSelectionFirstFive, allowSelectionFirstFive)); + add(new Div(new Span("Selected items: "), selectedItems)); + } + + private void updateSelection( + SelectionEvent, Integer> selectionEvent) { + String items = selectionEvent.getAllSelectedItems().stream() + .map(Object::toString).collect(Collectors.joining(",")); + selectedItems.setText(items); + } +} diff --git a/vaadin-grid-flow-parent/vaadin-grid-flow-integration-tests/src/test/java/com/vaadin/flow/component/grid/it/ConditionalSelectionIT.java b/vaadin-grid-flow-parent/vaadin-grid-flow-integration-tests/src/test/java/com/vaadin/flow/component/grid/it/ConditionalSelectionIT.java new file mode 100644 index 00000000000..781fcf0e188 --- /dev/null +++ b/vaadin-grid-flow-parent/vaadin-grid-flow-integration-tests/src/test/java/com/vaadin/flow/component/grid/it/ConditionalSelectionIT.java @@ -0,0 +1,120 @@ +/* + * Copyright 2000-2024 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.flow.component.grid.it; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import com.vaadin.flow.component.grid.testbench.GridElement; +import com.vaadin.flow.testutil.TestPath; +import com.vaadin.testbench.TestBenchElement; +import com.vaadin.tests.AbstractComponentIT; + +@TestPath("vaadin-grid/conditional-selection") +public class ConditionalSelectionIT extends AbstractComponentIT { + private GridElement grid; + + @Before + public void init() { + open(); + grid = $(GridElement.class).waitForFirst(); + } + + @Test + public void singleSelect_clickRow_preventsSelection() { + $("button").id("enable-single-selection").click(); + $("button").id("disable-selection-first-five").click(); + + // Prevents selection of non-selectable item + grid.select(0); + assertSelectedItems(Set.of()); + + // Allows selection of selectable item + grid.select(5); + assertSelectedItems(Set.of(5)); + } + + @Test + public void singleSelect_clickRow_preventsDeselection() { + $("button").id("enable-single-selection").click(); + grid.select(0); + + $("button").id("disable-selection-first-five").click(); + + // Prevents deselection of non-selectable item + grid.deselect(0); + assertSelectedItems(Set.of(0)); + + // Allows deselection of selectable item + grid.select(5); + grid.deselect(5); + assertSelectedItems(Set.of()); + } + + @Test + public void multiSelect_hidesCheckboxes() { + $("button").id("enable-multi-selection").click(); + $("button").id("disable-selection-first-five").click(); + + Assert.assertFalse(getItemCheckbox(0).isDisplayed()); + Assert.assertTrue(getItemCheckbox(5).isDisplayed()); + } + + @Test + public void multiSelect_updateProvider_updatesCheckboxes() { + $("button").id("enable-multi-selection").click(); + $("button").id("disable-selection-first-five").click(); + + Assert.assertFalse(getItemCheckbox(0).isDisplayed()); + Assert.assertTrue(getItemCheckbox(5).isDisplayed()); + + $("button").id("allow-selection-first-five").click(); + + Assert.assertTrue(getItemCheckbox(0).isDisplayed()); + Assert.assertFalse(getItemCheckbox(5).isDisplayed()); + } + + private TestBenchElement getItemCheckbox(int index) { + return grid.getCell(index, 0).$("vaadin-checkbox").first(); + } + + private Set getServerSelectedItems() { + var items = $("span").id("selected-items").getText(); + return items.isEmpty() ? Set.of() + : Stream.of(items.split(",")).map(Integer::parseInt) + .collect(Collectors.toSet()); + } + + @SuppressWarnings("unchecked") + private Set getClientSelectedItems() { + var itemNames = (List) getCommandExecutor().executeScript( + "return arguments[0].selectedItems.map(item => item.col0)", + grid); + return itemNames.stream().map(Integer::parseInt) + .collect(Collectors.toSet()); + } + + private void assertSelectedItems(Set items) { + Assert.assertEquals(items, getServerSelectedItems()); + Assert.assertEquals(items, getClientSelectedItems()); + } +} diff --git a/vaadin-grid-flow-parent/vaadin-grid-flow-integration-tests/test/grid-connector-selection.test.ts b/vaadin-grid-flow-parent/vaadin-grid-flow-integration-tests/test/grid-connector-selection.test.ts index 4bb35824d9c..e8c7b8f5d52 100644 --- a/vaadin-grid-flow-parent/vaadin-grid-flow-integration-tests/test/grid-connector-selection.test.ts +++ b/vaadin-grid-flow-parent/vaadin-grid-flow-integration-tests/test/grid-connector-selection.test.ts @@ -141,6 +141,95 @@ describe('grid connector - selection', () => { expect(grid.$server.deselect).not.to.be.called; }); }); + + describe('conditional selection', () => { + let items; + + beforeEach(async () => { + items = Array.from({ length: 4 }, (_, i) => ({ + key: i.toString(), + name: i.toString(), + selectable: i >= 2 + })); + setRootItems(grid.$connector, items); + await nextFrame(); + grid.requestContentUpdate(); + }); + + it('should prevent selection of non-selectable items on click', () => { + getBodyCellContent(grid, 0, 0)!.click(); + expect(grid.selectedItems).to.be.empty; + expect(grid.$server.select).to.not.be.called; + }); + + it('should allow selection of selectable items on click', async () => { + getBodyCellContent(grid, 2, 0)!.click(); + expect(grid.selectedItems).to.deep.equal([items[2]]); + expect(grid.$server.select).to.be.calledWith(items[2].key); + }); + + it('should prevent deselection of non-selectable items on click', () => { + grid.$connector.doSelection([items[0]], false); + getBodyCellContent(grid, 0, 0)!.click(); + expect(grid.selectedItems).to.deep.equal([items[0]]); + expect(grid.$server.deselect).to.not.be.called; + }); + + it('should prevent deselection of non-selectable items when clicking another non-selectable item', () => { + grid.$connector.doSelection([items[0]], false); + getBodyCellContent(grid, 1, 0)!.click(); + expect(grid.selectedItems).to.deep.equal([items[0]]); + expect(grid.$server.deselect).to.not.be.called; + }); + + it('should prevent deselection of non-selectable items on row click when active item data is stale', () => { + // item is selectable initially and is selected + grid.$connector.doSelection([items[2]], false); + + // update grid items to make the item non-selectable + const updatedItems = items.map((item) => ({ ...item, selectable: false })); + setRootItems(grid.$connector, updatedItems); + + // active item still references the original item with selectable: true + expect(grid.activeItem.selectable).to.be.true; + + // however clicking the row should not deselect the item + getBodyCellContent(grid, 2, 0)!.click(); + expect(grid.selectedItems).to.deep.equal([updatedItems[2]]); + expect(grid.$server.deselect).to.not.be.called; + }); + + it('should allow deselection of selectable items on row click', () => { + grid.$connector.doSelection([items[2]], false); + getBodyCellContent(grid, 2, 0)!.click(); + expect(grid.selectedItems).to.be.empty; + expect(grid.$server.deselect).to.be.calledWith(items[2].key); + }); + + it('should always allow selection from server', () => { + // non-selectable item + grid.$connector.doSelection([items[0]], false); + expect(grid.selectedItems).to.deep.equal([items[0]]); + expect(grid.activeItem).to.deep.equal(items[0]); + + // selectable item + grid.$connector.doSelection([items[2]], false); + expect(grid.selectedItems).to.deep.equal([items[2]]); + expect(grid.activeItem).to.deep.equal(items[2]); + }) + + it('should always allow deselection from server', () => { + // non-selectable item + grid.$connector.doSelection([items[0]], false); + grid.$connector.doDeselection([items[0]], false); + expect(grid.selectedItems).to.deep.equal([]); + + // selectable item + grid.$connector.doSelection([items[2]], false); + grid.$connector.doDeselection([items[2]], false); + expect(grid.selectedItems).to.deep.equal([]); + }) + }); }); describe('none selection mode', () => { diff --git a/vaadin-grid-flow-parent/vaadin-grid-flow-integration-tests/test/shared.ts b/vaadin-grid-flow-parent/vaadin-grid-flow-integration-tests/test/shared.ts index 7194359ff81..6163c4a42c2 100644 --- a/vaadin-grid-flow-parent/vaadin-grid-flow-integration-tests/test/shared.ts +++ b/vaadin-grid-flow-parent/vaadin-grid-flow-integration-tests/test/shared.ts @@ -46,6 +46,7 @@ export type Item = { key: string; name?: string; price?: number, + selectable?: boolean; selected?: boolean; detailsOpened?: boolean; style?: Record; diff --git a/vaadin-grid-flow-parent/vaadin-grid-flow/src/main/java/com/vaadin/flow/component/grid/AbstractGridMultiSelectionModel.java b/vaadin-grid-flow-parent/vaadin-grid-flow/src/main/java/com/vaadin/flow/component/grid/AbstractGridMultiSelectionModel.java index aee93172d87..ff085bba7e0 100644 --- a/vaadin-grid-flow-parent/vaadin-grid-flow/src/main/java/com/vaadin/flow/component/grid/AbstractGridMultiSelectionModel.java +++ b/vaadin-grid-flow-parent/vaadin-grid-flow/src/main/java/com/vaadin/flow/component/grid/AbstractGridMultiSelectionModel.java @@ -75,8 +75,7 @@ public AbstractGridMultiSelectionModel(Grid grid) { this::clientDeselectAll); selectAllCheckBoxVisibility = SelectAllCheckboxVisibility.DEFAULT; - selectionColumn - .setSelectAllCheckBoxVisibility(isSelectAllCheckboxVisible()); + updateSelectAllCheckBoxVisibility(); if (grid.getElement().getNode().isAttached()) { this.insertSelectionColumn(grid, selectionColumn); @@ -89,6 +88,11 @@ public AbstractGridMultiSelectionModel(Grid grid) { } } + void updateSelectAllCheckBoxVisibility() { + selectionColumn + .setSelectAllCheckBoxVisibility(isSelectAllCheckboxVisible()); + } + private void insertSelectionColumn(Grid grid, GridSelectionColumn selectionColumn) { grid.getElement().insertChild(0, selectionColumn.getElement()); @@ -105,7 +109,8 @@ protected void remove() { @Override public void selectFromClient(T item) { - if (isSelected(item)) { + boolean selectable = getGrid().isItemSelectable(item); + if (isSelected(item) || !selectable) { return; } @@ -131,7 +136,8 @@ public void selectFromClient(T item) { @Override public void deselectFromClient(T item) { - if (!isSelected(item)) { + boolean selectable = getGrid().isItemSelectable(item); + if (!isSelected(item) || !selectable) { return; } @@ -320,6 +326,10 @@ public SelectAllCheckboxVisibility getSelectAllCheckboxVisibility() { @Override public boolean isSelectAllCheckboxVisible() { + if (getGrid().getItemSelectableProvider() != null) { + return false; + } + switch (selectAllCheckBoxVisibility) { case DEFAULT: return getGrid().getDataCommunicator().getDataProvider() @@ -376,8 +386,8 @@ protected abstract void fireSelectionEvent( SelectionEvent, T> event); protected void clientSelectAll() { + // ignore call if the checkbox is hidden if (!isSelectAllCheckboxVisible()) { - // ignore event if the checkBox was meant to be hidden return; } Stream allItemsStream; @@ -439,8 +449,8 @@ private Stream fetchAllDescendants(T parent, } protected void clientDeselectAll() { + // ignore call if the checkbox is hidden if (!isSelectAllCheckboxVisible()) { - // ignore event if the checkBox was meant to be hidden return; } doUpdateSelection(Collections.emptySet(), getSelectedItems(), true); diff --git a/vaadin-grid-flow-parent/vaadin-grid-flow/src/main/java/com/vaadin/flow/component/grid/AbstractGridSingleSelectionModel.java b/vaadin-grid-flow-parent/vaadin-grid-flow/src/main/java/com/vaadin/flow/component/grid/AbstractGridSingleSelectionModel.java index 98aee4cbcc4..5f978f066a0 100644 --- a/vaadin-grid-flow-parent/vaadin-grid-flow/src/main/java/com/vaadin/flow/component/grid/AbstractGridSingleSelectionModel.java +++ b/vaadin-grid-flow-parent/vaadin-grid-flow/src/main/java/com/vaadin/flow/component/grid/AbstractGridSingleSelectionModel.java @@ -60,7 +60,8 @@ public AbstractGridSingleSelectionModel(Grid grid) { @Override public void selectFromClient(T item) { - if (isSelected(item)) { + boolean selectable = getGrid().isItemSelectable(item); + if (isSelected(item) || !selectable) { return; } doSelect(item, true); @@ -78,8 +79,9 @@ public void select(T item) { @Override public void deselectFromClient(T item) { - if (isSelected(item) && isDeselectAllowed()) { - selectFromClient(null); + boolean selectable = getGrid().isItemSelectable(item); + if (isSelected(item) && selectable && isDeselectAllowed()) { + doSelect(null, true); } } diff --git a/vaadin-grid-flow-parent/vaadin-grid-flow/src/main/java/com/vaadin/flow/component/grid/Grid.java b/vaadin-grid-flow-parent/vaadin-grid-flow/src/main/java/com/vaadin/flow/component/grid/Grid.java index daeb7d6d542..48ccb24b011 100755 --- a/vaadin-grid-flow-parent/vaadin-grid-flow/src/main/java/com/vaadin/flow/component/grid/Grid.java +++ b/vaadin-grid-flow-parent/vaadin-grid-flow/src/main/java/com/vaadin/flow/component/grid/Grid.java @@ -1413,6 +1413,7 @@ public UpdateQueueData getUpdateQueueData() { private GridSelectionModel selectionModel; private SelectionMode selectionMode; + private SerializablePredicate selectableProvider; private final DetailsManager detailsManager; @@ -1710,6 +1711,7 @@ protected > gridDataGenerator.addDataGenerator(this::generateTooltipTextData); gridDataGenerator.addDataGenerator(this::generateRowsDragAndDropAccess); gridDataGenerator.addDataGenerator(this::generateDragData); + gridDataGenerator.addDataGenerator(this::generateSelectableData); dataCommunicator = dataCommunicatorBuilder.build(getElement(), gridDataGenerator, (U) arrayUpdater, @@ -3068,6 +3070,41 @@ public GridSelectionModel setSelectionMode(SelectionMode selectionMode) { return model; } + SerializablePredicate getItemSelectableProvider() { + return selectableProvider; + } + + /** + * Sets a predicate to check whether a specific item in the grid may be + * selected or deselected by the user. The predicate receives an item + * instance and should return {@code true} if a user may change the + * selection state of that item, or {@code false} otherwise. + *

+ * This function does not prevent programmatic selection/deselection of + * items. Changing the function does not modify the currently selected + * items. + *

+ * When using multi-selection, setting a provider will hide the select all + * checkbox. + * + * @param provider + * the function to use to determine whether an item may be + * selected or deselected by the user, or {@code null} to allow + * all items to be selected or deselected + */ + public void setItemSelectableProvider(SerializablePredicate provider) { + selectableProvider = provider; + getDataCommunicator().reset(); + + if (selectionModel instanceof AbstractGridMultiSelectionModel multiSelectionModel) { + multiSelectionModel.updateSelectAllCheckBoxVisibility(); + } + } + + boolean isItemSelectable(T item) { + return selectableProvider == null || selectableProvider.test(item); + } + /** * Use this grid as a single select in {@link Binder}. *

@@ -4367,6 +4404,13 @@ private void generateDragData(T item, JsonObject jsonObject) { } } + private void generateSelectableData(T item, JsonObject jsonObject) { + if (selectableProvider != null) { + boolean selectable = selectableProvider.test(item); + jsonObject.put("selectable", selectable); + } + } + /** * Creates a new Editor instance. Can be overridden to create a custom * Editor. If the Editor is a {@link AbstractGridExtension}, it will be diff --git a/vaadin-grid-flow-parent/vaadin-grid-flow/src/main/java/com/vaadin/flow/component/grid/GridMultiSelectionModel.java b/vaadin-grid-flow-parent/vaadin-grid-flow/src/main/java/com/vaadin/flow/component/grid/GridMultiSelectionModel.java index 48669446f57..8a91d044b95 100644 --- a/vaadin-grid-flow-parent/vaadin-grid-flow/src/main/java/com/vaadin/flow/component/grid/GridMultiSelectionModel.java +++ b/vaadin-grid-flow-parent/vaadin-grid-flow/src/main/java/com/vaadin/flow/component/grid/GridMultiSelectionModel.java @@ -19,6 +19,7 @@ import com.vaadin.flow.data.selection.MultiSelect; import com.vaadin.flow.data.selection.MultiSelectionListener; import com.vaadin.flow.data.selection.SelectionModel; +import com.vaadin.flow.function.SerializablePredicate; import com.vaadin.flow.shared.Registration; /** @@ -125,7 +126,9 @@ void setSelectAllCheckboxVisibility( *

* The select all checkbox will never be shown if the Grid uses lazy loading * with unknown item count, meaning that no count callback has been - * provided. + * provided. It will also not be shown if the grid is configured to use + * conditional selection via + * {@link Grid#setItemSelectableProvider(SerializablePredicate)} * * @return {@code true} if the checkbox will be visible with the current * settings diff --git a/vaadin-grid-flow-parent/vaadin-grid-flow/src/main/resources/META-INF/resources/frontend/gridConnector.ts b/vaadin-grid-flow-parent/vaadin-grid-flow/src/main/resources/META-INF/resources/frontend/gridConnector.ts index 77a8f548ec4..20b3a1bd17c 100644 --- a/vaadin-grid-flow-parent/vaadin-grid-flow/src/main/resources/META-INF/resources/frontend/gridConnector.ts +++ b/vaadin-grid-flow-parent/vaadin-grid-flow/src/main/resources/META-INF/resources/frontend/gridConnector.ts @@ -116,8 +116,11 @@ window.Vaadin.Flow.gridConnector = { selectedKeys = {}; } + let selectedItemsChanged = false; items.forEach((item) => { - if (item) { + const selectable = !userOriginated || grid.isItemSelectable(item); + selectedItemsChanged = selectedItemsChanged || selectable; + if (item && selectable) { selectedKeys[item.key] = item; item.selected = true; if (userOriginated) { @@ -133,7 +136,9 @@ window.Vaadin.Flow.gridConnector = { } }); - grid.selectedItems = Object.values(selectedKeys); + if (selectedItemsChanged) { + grid.selectedItems = Object.values(selectedKeys); + } }); grid.$connector.doDeselection = tryCatchWrapper(function (items, userOriginated) { @@ -144,6 +149,10 @@ window.Vaadin.Flow.gridConnector = { const updatedSelectedItems = grid.selectedItems.slice(); while (items.length) { const itemToDeselect = items.shift(); + const selectable = !userOriginated || grid.isItemSelectable(itemToDeselect); + if (!selectable) { + continue; + } for (let i = 0; i < updatedSelectedItems.length; i++) { const selectedItem = updatedSelectedItems[i]; if (itemToDeselect?.key === selectedItem.key) { @@ -171,6 +180,11 @@ window.Vaadin.Flow.gridConnector = { if (grid.__deselectDisallowed) { grid.activeItem = oldVal; } else { + // The item instance may have changed since the item was stored as active item + // and information such as whether the item may be selected or deselected may + // be stale. Use data provider controller to get updated instance from grid + // cache. + oldVal = dataProviderController.getItemContext(oldVal).item; grid.$connector.doDeselection([oldVal], true); } } @@ -1189,5 +1203,10 @@ window.Vaadin.Flow.gridConnector = { } }) ); + + grid.isItemSelectable = tryCatchWrapper((item) => { + // If there is no selectable data, assume the item is selectable + return item?.selectable === undefined || item.selectable; + }); })(grid) }; diff --git a/vaadin-grid-flow-parent/vaadin-grid-flow/src/test/java/com/vaadin/flow/component/grid/AbstractGridMultiSelectionModelTest.java b/vaadin-grid-flow-parent/vaadin-grid-flow/src/test/java/com/vaadin/flow/component/grid/AbstractGridMultiSelectionModelTest.java index 51e0c6fd744..83baba9a24a 100644 --- a/vaadin-grid-flow-parent/vaadin-grid-flow/src/test/java/com/vaadin/flow/component/grid/AbstractGridMultiSelectionModelTest.java +++ b/vaadin-grid-flow-parent/vaadin-grid-flow/src/test/java/com/vaadin/flow/component/grid/AbstractGridMultiSelectionModelTest.java @@ -471,6 +471,181 @@ public void setFilterUsingDataView_serverSelectAll_selectionEventContainsFiltere grid.getSelectedItems().contains(items.get(0))); } + @Test + public void selectFromClient_withItemSelectableProvider_preventsSelection() { + grid.setItems("foo", "bar"); + grid.setSelectionMode(SelectionMode.MULTI); + grid.setItemSelectableProvider(item -> !item.equals("foo")); + + AbstractGridMultiSelectionModel selectionModel = (AbstractGridMultiSelectionModel) grid + .getSelectionModel(); + + // prevents selection of non-selectable item + selectionModel.selectFromClient("foo"); + Assert.assertEquals(Set.of(), grid.getSelectedItems()); + + // allows selection of selectable item + selectionModel.selectFromClient("bar"); + Assert.assertEquals(Set.of("bar"), grid.getSelectedItems()); + } + + @Test + public void deselectFromClient_withItemSelectableProvider_preventsDeselection() { + grid.setItems("foo", "bar"); + grid.setSelectionMode(SelectionMode.MULTI); + grid.setItemSelectableProvider(item -> !item.equals("foo")); + + AbstractGridMultiSelectionModel selectionModel = (AbstractGridMultiSelectionModel) grid + .getSelectionModel(); + + // prevents deselection of non-selectable item + selectionModel.select("foo"); + selectionModel.deselectFromClient("foo"); + Assert.assertEquals(Set.of("foo"), grid.getSelectedItems()); + + // allows deselection of selectable item + selectionModel.select("bar"); + selectionModel.deselectFromClient("bar"); + Assert.assertEquals(Set.of("foo"), grid.getSelectedItems()); + } + + @Test + public void select_withItemSelectableProvider_allowsSelection() { + grid.setItems("foo", "bar"); + grid.setSelectionMode(SelectionMode.MULTI); + grid.setItemSelectableProvider(item -> !item.equals("foo")); + + AbstractGridMultiSelectionModel selectionModel = (AbstractGridMultiSelectionModel) grid + .getSelectionModel(); + + // allows selection using select + selectionModel.select("foo"); + Assert.assertEquals(Set.of("foo"), grid.getSelectedItems()); + + // allows selection using selectItems + selectionModel.deselectAll(); + selectionModel.selectItems("foo", "bar"); + Assert.assertEquals(Set.of("foo", "bar"), grid.getSelectedItems()); + + // allows selection using updateSelection + selectionModel.deselectAll(); + selectionModel.updateSelection(Set.of("foo", "bar"), Set.of()); + Assert.assertEquals(Set.of("foo", "bar"), grid.getSelectedItems()); + } + + @Test + public void deselect_withItemSelectableProvider_allowsDeselection() { + grid.setItems("foo", "bar"); + grid.setSelectionMode(SelectionMode.MULTI); + grid.setItemSelectableProvider(item -> !item.equals("foo")); + + AbstractGridMultiSelectionModel selectionModel = (AbstractGridMultiSelectionModel) grid + .getSelectionModel(); + + // allows deselection using deselect + selectionModel.select("foo"); + selectionModel.deselect("foo"); + Assert.assertEquals(Set.of(), grid.getSelectedItems()); + + // allows deselection using deselectItems + selectionModel.selectItems("foo", "bar"); + selectionModel.deselectItems("foo", "bar"); + Assert.assertEquals(Set.of(), grid.getSelectedItems()); + + // allows deselection using updateSelection + selectionModel.updateSelection(Set.of("foo", "bar"), Set.of()); + selectionModel.updateSelection(Set.of(), Set.of("foo", "bar")); + Assert.assertEquals(Set.of(), grid.getSelectedItems()); + } + + @Test + public void selectAll_withItemSelectableProvider_works() { + grid.setItems("foo", "bar"); + grid.setSelectionMode(SelectionMode.MULTI); + grid.setItemSelectableProvider(item -> true); + + AbstractGridMultiSelectionModel selectionModel = (AbstractGridMultiSelectionModel) grid + .getSelectionModel(); + + selectionModel.selectAll(); + + Assert.assertEquals(2, selectionModel.getSelectedItems().size()); + } + + @Test + public void deselectAll_withItemSelectableProvider_works() { + grid.setItems("foo", "bar"); + grid.setSelectionMode(SelectionMode.MULTI); + grid.setItemSelectableProvider(item -> true); + + AbstractGridMultiSelectionModel selectionModel = (AbstractGridMultiSelectionModel) grid + .getSelectionModel(); + + selectionModel.selectAll(); + selectionModel.deselectAll(); + + Assert.assertEquals(0, selectionModel.getSelectedItems().size()); + } + + @Test + public void clientSelectAll_withItemSelectableProvider_ignored() { + grid.setItems("foo", "bar"); + grid.setSelectionMode(SelectionMode.MULTI); + grid.setItemSelectableProvider(item -> true); + + AbstractGridMultiSelectionModel selectionModel = (AbstractGridMultiSelectionModel) grid + .getSelectionModel(); + + selectionModel.clientSelectAll(); + + Assert.assertEquals(0, selectionModel.getSelectedItems().size()); + } + + @Test + public void clientDeselectAll_withItemSelectableProvider_ignored() { + grid.setItems("foo", "bar"); + grid.setSelectionMode(SelectionMode.MULTI); + grid.setItemSelectableProvider(item -> true); + + AbstractGridMultiSelectionModel selectionModel = (AbstractGridMultiSelectionModel) grid + .getSelectionModel(); + selectionModel.selectAll(); + + selectionModel.clientSelectAll(); + + Assert.assertEquals(2, selectionModel.getSelectedItems().size()); + } + + @Test + public void setItemSelectableProvider_updatesSelectAllVisibility() { + grid.setSelectionMode(SelectionMode.MULTI); + + AbstractGridMultiSelectionModel selectionModel = (AbstractGridMultiSelectionModel) grid + .getSelectionModel(); + GridSelectionColumn selectionColumn = selectionModel + .getSelectionColumn(); + + // Visible initially + Assert.assertFalse(selectionColumn.getElement() + .getProperty("_selectAllHidden", false)); + + // Set provider, should hide select all checkbox + grid.setItemSelectableProvider(item -> false); + Assert.assertTrue(selectionColumn.getElement() + .getProperty("_selectAllHidden", false)); + + // Try to explicitly make the checkbox visible, should still be hidden + selectionModel.setSelectAllCheckboxVisibility( + GridMultiSelectionModel.SelectAllCheckboxVisibility.VISIBLE); + Assert.assertTrue(selectionColumn.getElement() + .getProperty("_selectAllHidden", false)); + + // Remove provider, should show select all checkbox + grid.setItemSelectableProvider(null); + Assert.assertFalse(selectionColumn.getElement() + .getProperty("_selectAllHidden", false)); + } + private void verifySelectAllCheckboxVisibilityInMultiSelectMode( boolean inMemory, boolean unknownItemCount, boolean expectedVisibility, diff --git a/vaadin-grid-flow-parent/vaadin-grid-flow/src/test/java/com/vaadin/flow/component/grid/AbstractGridSingleSelectionModelTest.java b/vaadin-grid-flow-parent/vaadin-grid-flow/src/test/java/com/vaadin/flow/component/grid/AbstractGridSingleSelectionModelTest.java index af874c3c953..13c9086fee9 100644 --- a/vaadin-grid-flow-parent/vaadin-grid-flow/src/test/java/com/vaadin/flow/component/grid/AbstractGridSingleSelectionModelTest.java +++ b/vaadin-grid-flow-parent/vaadin-grid-flow/src/test/java/com/vaadin/flow/component/grid/AbstractGridSingleSelectionModelTest.java @@ -16,6 +16,7 @@ package com.vaadin.flow.component.grid; import java.util.Objects; +import java.util.Set; import java.util.stream.Stream; import org.junit.Assert; @@ -31,14 +32,17 @@ public class AbstractGridSingleSelectionModelTest { private CallbackDataProvider dataProviderWithIdentityProvider; private SelectionListener, TestEntity> selectionListenerMock; + private final TestEntity entity1 = new TestEntity(1, "Name"); + private final TestEntity entity2 = new TestEntity(2, "Name"); + private final TestEntity entity3 = new TestEntity(3, "Name"); + @Before @SuppressWarnings("unchecked") public void setup() { grid = new Grid<>(); dataProviderWithIdentityProvider = new CallbackDataProvider<>( - query -> Stream.of(new TestEntity(1, "Name"), - new TestEntity(2, "Name"), new TestEntity(3, "Name")), - query -> 3, TestEntity::getId); + query -> Stream.of(entity1, entity2, entity3), query -> 3, + TestEntity::getId); selectionListenerMock = Mockito.mock(SelectionListener.class); grid.getSelectionModel().addSelectionListener(selectionListenerMock); } @@ -120,6 +124,76 @@ public void isSelected_usesDataProviderIdentify() { selectionModel.isSelected(new TestEntity(1, "Joseph"))); } + @Test + public void selectFromClient_withItemSelectableProvider_preventsSelection() { + grid.setItems(dataProviderWithIdentityProvider); + grid.setItemSelectableProvider(item -> item.getId() != entity1.id); + + GridSelectionModel selectionModel = grid + .getSelectionModel(); + + // prevent client selection of non-selectable item + selectionModel.selectFromClient(entity1); + Assert.assertEquals(Set.of(), selectionModel.getSelectedItems()); + + // allow client selection of selectable item + selectionModel.selectFromClient(entity2); + Assert.assertEquals(Set.of(entity2), selectionModel.getSelectedItems()); + } + + @Test + public void deselectFromClient_withItemSelectableProvider_preventsDeselection() { + grid.setItems(dataProviderWithIdentityProvider); + grid.setItemSelectableProvider(item -> item.getId() != entity1.id); + + GridSelectionModel selectionModel = grid + .getSelectionModel(); + + // prevent client deselection of non-selectable item + selectionModel.select(entity1); + selectionModel.deselectFromClient(entity1); + Assert.assertEquals(Set.of(entity1), selectionModel.getSelectedItems()); + + // allow client deselection of selectable item + selectionModel.select(entity2); + selectionModel.deselectFromClient(entity2); + Assert.assertEquals(Set.of(), selectionModel.getSelectedItems()); + } + + @Test + public void select_withItemSelectableProvider_allowsSelection() { + grid.setItems(dataProviderWithIdentityProvider); + grid.setItemSelectableProvider(item -> item.getId() != entity1.id); + + GridSelectionModel selectionModel = grid + .getSelectionModel(); + + // allow programmatic selection of any item + selectionModel.select(entity1); + Assert.assertEquals(Set.of(entity1), selectionModel.getSelectedItems()); + + selectionModel.select(entity2); + Assert.assertEquals(Set.of(entity2), selectionModel.getSelectedItems()); + } + + @Test + public void deselect_withItemSelectableProvider_allowsDeselection() { + grid.setItems(dataProviderWithIdentityProvider); + grid.setItemSelectableProvider(item -> item.getId() != entity1.id); + + GridSelectionModel selectionModel = grid + .getSelectionModel(); + + // allow programmatic deselection of any item + selectionModel.select(entity1); + selectionModel.deselect(entity1); + Assert.assertEquals(Set.of(), selectionModel.getSelectedItems()); + + selectionModel.select(entity2); + selectionModel.select(entity2); + Assert.assertEquals(Set.of(entity2), selectionModel.getSelectedItems()); + } + public static class TestEntity { private int id; private String name;