diff options
author | Pekka Hyvönen <pekka@vaadin.com> | 2016-11-11 09:41:43 +0200 |
---|---|---|
committer | Vaadin Code Review <review@vaadin.com> | 2016-11-29 10:18:11 +0000 |
commit | f2d8f812efa067b4baa7e27c0ea76f7596b291e6 (patch) | |
tree | 8e35e6c4eef4ffc5f8006d30989914da1deeab1b /server/src | |
parent | 13443562ccbd633ceb561bb87893014f65437ad1 (diff) | |
download | vaadin-framework-f2d8f812efa067b4baa7e27c0ea76f7596b291e6.tar.gz vaadin-framework-f2d8f812efa067b4baa7e27c0ea76f7596b291e6.zip |
Add MultiSelect support for Grid
Still missing following things coming in next patches:
- select all checkbox
- firing an event when data provider is changed in grid
- read only selection models for grid
Part 1 for vaadin/framework8-issues#232
Change-Id: Ib2c7c81a838f43cb7c521a56d50139c91961f54a
Diffstat (limited to 'server/src')
-rw-r--r-- | server/src/main/java/com/vaadin/data/SelectionModel.java | 27 | ||||
-rw-r--r-- | server/src/main/java/com/vaadin/event/selection/MultiSelectionEvent.java | 33 | ||||
-rw-r--r-- | server/src/main/java/com/vaadin/ui/Grid.java | 75 | ||||
-rw-r--r-- | server/src/main/java/com/vaadin/ui/components/grid/MultiSelectionModelImpl.java | 277 | ||||
-rw-r--r-- | server/src/main/java/com/vaadin/ui/components/grid/SingleSelectionModelImpl.java (renamed from server/src/main/java/com/vaadin/ui/components/grid/SingleSelectionModel.java) | 34 | ||||
-rw-r--r-- | server/src/test/java/com/vaadin/data/GridAsMultiSelectInBinder.java | 270 | ||||
-rw-r--r-- | server/src/test/java/com/vaadin/data/GridAsSingleSelectInBinder.java | 14 | ||||
-rw-r--r-- | server/src/test/java/com/vaadin/tests/components/grid/GridMultiSelectionModelTest.java | 543 | ||||
-rw-r--r-- | server/src/test/java/com/vaadin/tests/components/grid/GridSingleSelectionModelTest.java | 113 |
9 files changed, 1329 insertions, 57 deletions
diff --git a/server/src/main/java/com/vaadin/data/SelectionModel.java b/server/src/main/java/com/vaadin/data/SelectionModel.java index 98a068993a..cf4fb846b6 100644 --- a/server/src/main/java/com/vaadin/data/SelectionModel.java +++ b/server/src/main/java/com/vaadin/data/SelectionModel.java @@ -72,10 +72,15 @@ public interface SelectionModel<T> extends Serializable { if (item != null) { select(item); } else { - deselectAll(); + getSelectedItem().ifPresent(this::deselect); } } + @Override + public default void deselectAll() { + setSelectedItem(null); + } + /** * Returns a singleton set of the currently selected item or an empty * set if no item is selected. @@ -145,6 +150,12 @@ public interface SelectionModel<T> extends Serializable { Collections.emptySet()); } + @SuppressWarnings("unchecked") + @Override + public default void deselect(T item) { + deselectItems(item); + } + /** * Removes the given items from the set of currently selected items. * <p> @@ -183,6 +194,14 @@ public interface SelectionModel<T> extends Serializable { default Optional<T> getFirstSelectedItem() { return getSelectedItems().stream().findFirst(); } + + /** + * Deselects all currently selected items. + */ + @Override + public default void deselectAll() { + updateSelection(Collections.emptySet(), getSelectedItems()); + } } /** @@ -227,11 +246,9 @@ public interface SelectionModel<T> extends Serializable { public void deselect(T item); /** - * Deselects all currently selected items. + * Deselects all currently selected items, if any. */ - public default void deselectAll() { - getSelectedItems().forEach(this::deselect); - } + public void deselectAll(); /** * Returns whether the given item is currently selected. diff --git a/server/src/main/java/com/vaadin/event/selection/MultiSelectionEvent.java b/server/src/main/java/com/vaadin/event/selection/MultiSelectionEvent.java index 5fb91bbcbe..7718b5a1a6 100644 --- a/server/src/main/java/com/vaadin/event/selection/MultiSelectionEvent.java +++ b/server/src/main/java/com/vaadin/event/selection/MultiSelectionEvent.java @@ -21,6 +21,8 @@ import java.util.Set; import com.vaadin.data.HasValue.ValueChangeEvent; import com.vaadin.ui.AbstractMultiSelect; +import com.vaadin.ui.Component; +import com.vaadin.ui.MultiSelect; /** * Event fired when the the selection changes in a @@ -56,12 +58,31 @@ public class MultiSelectionEvent<T> extends ValueChangeEvent<Set<T>> } /** + * Creates a new selection change event in a multiselect component. + * + * @param component + * the component + * @param source + * the multiselect source + * @param oldSelection + * the old set of selected items + * @param userOriginated + * {@code true} if this event originates from the client, + * {@code false} otherwise. + */ + public MultiSelectionEvent(Component component, MultiSelect<T> source, + Set<T> oldSelection, boolean userOriginated) { + super(component, source, userOriginated); + this.oldSelection = oldSelection; + } + + /** * Gets the new selection. * <p> * The result is the current selection of the source * {@link AbstractMultiSelect} object. So it's always exactly the same as * {@link AbstractMultiSelect#getValue()} - * + * * @see #getValue() * * @return a set of items selected after the selection was changed @@ -83,4 +104,14 @@ public class MultiSelectionEvent<T> extends ValueChangeEvent<Set<T>> public Optional<T> getFirstSelected() { return getValue().stream().findFirst(); } + + /** + * The multiselect on which the Event initially occurred. + * + * @return the multiselect on which the Event initially occurred. + */ + @Override + public MultiSelect<T> getSource() { + return (MultiSelect<T>) super.getSource(); + } } diff --git a/server/src/main/java/com/vaadin/ui/Grid.java b/server/src/main/java/com/vaadin/ui/Grid.java index e6830f3b95..5010387377 100644 --- a/server/src/main/java/com/vaadin/ui/Grid.java +++ b/server/src/main/java/com/vaadin/ui/Grid.java @@ -72,7 +72,7 @@ import com.vaadin.ui.components.grid.EditorImpl; import com.vaadin.ui.components.grid.Footer; import com.vaadin.ui.components.grid.Header; import com.vaadin.ui.components.grid.Header.Row; -import com.vaadin.ui.components.grid.SingleSelectionModel; +import com.vaadin.ui.components.grid.SingleSelectionModelImpl; import com.vaadin.ui.declarative.DesignContext; import com.vaadin.ui.renderers.AbstractRenderer; import com.vaadin.ui.renderers.Renderer; @@ -133,9 +133,12 @@ public class Grid<T> extends AbstractListing<T> /** * The server-side interface that controls Grid's selection state. * SelectionModel should extend {@link AbstractGridExtension}. + * <p> * * @param <T> * the grid bean type + * @see SingleSelectionModel + * @see MultiSelectionModel */ public interface GridSelectionModel<T> extends SelectionModel<T>, Extension { @@ -152,6 +155,42 @@ public class Grid<T> extends AbstractListing<T> } /** + * Single selection model interface for Grid. + * + * @param <T> + * the type of items in grid + */ + public interface SingleSelectionModel<T> extends GridSelectionModel<T>, + com.vaadin.data.SelectionModel.Single<T> { + + /** + * Gets a wrapper to use this single selection model as a single select + * in {@link Binder}. + * + * @return the single select wrapper + */ + SingleSelect<T> asSingleSelect(); + } + + /** + * Multiselection model interface for Grid. + * + * @param <T> + * the type of items in grid + */ + public interface MultiSelectionModel<T> extends GridSelectionModel<T>, + com.vaadin.data.SelectionModel.Multi<T> { + + /** + * Gets a wrapper to use this multiselection model as a multiselect in + * {@link Binder}. + * + * @return the multiselect wrapper + */ + MultiSelect<T> asMultiSelect(); + } + + /** * An event listener for column resize events in the Grid. */ @FunctionalInterface @@ -2030,7 +2069,7 @@ public class Grid<T> extends AbstractListing<T> setDefaultHeaderRow(appendHeaderRow()); - selectionModel = new SingleSelectionModel<>(this); + selectionModel = new SingleSelectionModelImpl<>(this); detailsManager = new DetailsManager<>(); addExtension(detailsManager); @@ -2896,15 +2935,18 @@ public class Grid<T> extends AbstractListing<T> /** * Use this grid as a single select in {@link Binder}. * <p> - * Sets the grid to single select mode, if not yet so. + * Throws {@link IllegalStateException} if the grid is not using a + * {@link SingleSelectionModel}. * * @return the single select wrapper that can be used in binder + * @throws IllegalStateException + * if not using a single selection model */ public SingleSelect<T> asSingleSelect() { GridSelectionModel<T> model = getSelectionModel(); if (!(model instanceof SingleSelectionModel)) { - model = new SingleSelectionModel<>(this); - setSelectionModel(model); + throw new IllegalStateException( + "Grid is not in single select mode, it needs to be explicitly set to such with setSelectionModel(SingleSelectionModel) before being able to use single selection features."); } return ((SingleSelectionModel<T>) model).asSingleSelect(); @@ -2915,14 +2957,33 @@ public class Grid<T> extends AbstractListing<T> } /** + * User this grid as a multiselect in {@link Binder}. + * <p> + * Throws {@link IllegalStateException} if the grid is not using a + * {@link MultiSelectionModel}. + * + * @return the multiselect wrapper that can be used in binder + * @throws IllegalStateException + * if not using a multiselection model + */ + public MultiSelect<T> asMultiSelect() { + GridSelectionModel<T> model = getSelectionModel(); + if (!(model instanceof MultiSelectionModel)) { + throw new IllegalStateException( + "Grid is not in multiselect mode, it needs to be explicitly set to such with setSelectionModel(MultiSelectionModel) before being able to use multiselection features."); + } + return ((MultiSelectionModel<T>) model).asMultiSelect(); + } + + /** * Sets the selection model for this listing. * <p> - * The default selection model is {@link SingleSelectionModel}. + * The default selection model is {@link SingleSelectionModelImpl}. * * @param model * the selection model to use, not {@code null} */ - protected void setSelectionModel(GridSelectionModel<T> model) { + public void setSelectionModel(GridSelectionModel<T> model) { Objects.requireNonNull(model, "selection model cannot be null"); selectionModel.remove(); selectionModel = model; diff --git a/server/src/main/java/com/vaadin/ui/components/grid/MultiSelectionModelImpl.java b/server/src/main/java/com/vaadin/ui/components/grid/MultiSelectionModelImpl.java new file mode 100644 index 0000000000..286edd5f2b --- /dev/null +++ b/server/src/main/java/com/vaadin/ui/components/grid/MultiSelectionModelImpl.java @@ -0,0 +1,277 @@ +/* + * Copyright 2000-2016 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.ui.components.grid; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Objects; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import com.vaadin.event.selection.MultiSelectionEvent; +import com.vaadin.event.selection.MultiSelectionListener; +import com.vaadin.shared.Registration; +import com.vaadin.shared.data.DataCommunicatorConstants; +import com.vaadin.shared.data.selection.GridMultiSelectServerRpc; +import com.vaadin.ui.Grid; +import com.vaadin.ui.Grid.AbstractGridExtension; +import com.vaadin.ui.Grid.MultiSelectionModel; +import com.vaadin.ui.MultiSelect; +import com.vaadin.util.ReflectTools; + +import elemental.json.JsonObject; + +/** + * Multiselection model for grid. + * <p> + * Shows a column of checkboxes as the first column of grid. Each checkbox + * triggers the selection for that row. + * + * @author Vaadin Ltd. + * @since 8.0 + * + * @param <T> + * the type of the selected item in grid. + */ +public class MultiSelectionModelImpl<T> extends AbstractGridExtension<T> + implements MultiSelectionModel<T> { + + private class GridMultiSelectServerRpcImpl + implements GridMultiSelectServerRpc { + + @Override + public void select(String key) { + MultiSelectionModelImpl.this.updateSelection( + new LinkedHashSet<>(Arrays.asList(getData(key))), + Collections.emptySet(), true); + } + + @Override + public void deselect(String key) { + MultiSelectionModelImpl.this.updateSelection(Collections.emptySet(), + new LinkedHashSet<>(Arrays.asList(getData(key))), true); + } + + @Override + public void selectAll() { + // TODO will be added in another patch + throw new UnsupportedOperationException("Select all not supported"); + } + + @Override + public void deselectAll() { + // TODO will be added in another patch + throw new UnsupportedOperationException( + "Deelect all not supported"); + } + } + + @Deprecated + private static final Method SELECTION_CHANGE_METHOD = ReflectTools + .findMethod(MultiSelectionListener.class, "accept", + MultiSelectionEvent.class); + + private final Grid<T> grid; + + private Set<T> selection = new LinkedHashSet<>(); + + /** + * Constructs a new multiselection model for the given grid. + * + * @param grid + * the grid to bind the selection model into + */ + public MultiSelectionModelImpl(Grid<T> grid) { + this.grid = grid; + extend(grid); + + registerRpc(new GridMultiSelectServerRpcImpl()); + } + + @Override + public void remove() { + updateSelection(Collections.emptySet(), getSelectedItems(), false); + + super.remove(); + } + + /** + * Adds a selection listener that will be called when the selection is + * changed either by the user or programmatically. + * + * @param listener + * the value change listener, not {@code null} + * @return a registration for the listener + */ + public Registration addSelectionListener( + MultiSelectionListener<T> listener) { + addListener(MultiSelectionEvent.class, listener, + SELECTION_CHANGE_METHOD); + return () -> removeListener(MultiSelectionEvent.class, listener); + } + + @Override + public void generateData(T item, JsonObject jsonObject) { + // in case of all items selected, don't write individual items as + // seleted + if (isSelected(item)) { + jsonObject.put(DataCommunicatorConstants.SELECTED, true); + } + } + + @Override + public Set<T> getSelectedItems() { + return Collections.unmodifiableSet(new LinkedHashSet<>(selection)); + } + + @Override + public void updateSelection(Set<T> addedItems, Set<T> removedItems) { + updateSelection(addedItems, removedItems, false); + } + + /** + * Gets a wrapper for using this grid as a multiselect in a binder. + * + * @return a multiselect wrapper for grid + */ + @Override + public MultiSelect<T> asMultiSelect() { + return new MultiSelect<T>() { + + @Override + public void setValue(Set<T> value) { + Objects.requireNonNull(value); + Set<T> copy = value.stream().map(Objects::requireNonNull) + .collect(Collectors.toCollection(LinkedHashSet::new)); + + updateSelection(copy, new LinkedHashSet<>(getSelectedItems())); + } + + @Override + public Set<T> getValue() { + return getSelectedItems(); + } + + @Override + public Registration addValueChangeListener( + com.vaadin.data.HasValue.ValueChangeListener<Set<T>> listener) { + return addSelectionListener(event -> listener.accept(event)); + } + + @Override + public void setRequiredIndicatorVisible( + boolean requiredIndicatorVisible) { + // TODO support required indicator for grid ? + throw new UnsupportedOperationException( + "Required indicator is not supported in grid."); + } + + @Override + public boolean isRequiredIndicatorVisible() { + // TODO support required indicator for grid ? + throw new UnsupportedOperationException( + "Required indicator is not supported in grid."); + } + + @Override + public void setReadOnly(boolean readOnly) { + // TODO support read only in grid ? + throw new UnsupportedOperationException( + "Read only mode is not supported for grid."); + } + + @Override + public boolean isReadOnly() { + // TODO support read only in grid ? + throw new UnsupportedOperationException( + "Read only mode is not supported for grid."); + } + + @Override + public void updateSelection(Set<T> addedItems, + Set<T> removedItems) { + MultiSelectionModelImpl.this.updateSelection(addedItems, + removedItems); + } + + @Override + public Set<T> getSelectedItems() { + return MultiSelectionModelImpl.this.getSelectedItems(); + } + + @Override + public Registration addSelectionListener( + MultiSelectionListener<T> listener) { + return MultiSelectionModelImpl.this + .addSelectionListener(listener); + } + }; + } + + /** + * Updates the selection by adding and removing the given items. + * <p> + * All selection updates should go through this method, since it handles + * incorrect parameters, removing duplicates, notifying data communicator + * and and firing events. + * + * @param addedItems + * the items added to selection, not {@code} null + * @param removedItems + * the items removed from selection, not {@code} null + * @param userOriginated + * {@code true} if this was used originated, {@code false} if not + */ + protected void updateSelection(Set<T> addedItems, Set<T> removedItems, + boolean userOriginated) { + Objects.requireNonNull(addedItems); + Objects.requireNonNull(removedItems); + + // if there are duplicates, some item is both added & removed, just + // discard that and leave things as was before + addedItems.removeIf(item -> removedItems.remove(item)); + + if (selection.containsAll(addedItems) + && Collections.disjoint(selection, removedItems)) { + return; + } + + doUpdateSelection(set -> { + // order of add / remove does not matter since no duplicates + set.removeAll(removedItems); + set.addAll(addedItems); + removedItems.forEach(grid.getDataCommunicator()::refresh); + addedItems.forEach(grid.getDataCommunicator()::refresh); + }, userOriginated); + } + + private void doUpdateSelection(Consumer<Set<T>> handler, + boolean userOriginated) { + if (getParent() == null) { + throw new IllegalStateException( + "Trying to update selection for grid selection model that has been detached from the grid."); + } + + LinkedHashSet<T> oldSelection = new LinkedHashSet<>(selection); + handler.accept(selection); + + fireEvent(new MultiSelectionEvent<>(grid, asMultiSelect(), oldSelection, + userOriginated)); + } +} diff --git a/server/src/main/java/com/vaadin/ui/components/grid/SingleSelectionModel.java b/server/src/main/java/com/vaadin/ui/components/grid/SingleSelectionModelImpl.java index d095504768..e68487f358 100644 --- a/server/src/main/java/com/vaadin/ui/components/grid/SingleSelectionModel.java +++ b/server/src/main/java/com/vaadin/ui/components/grid/SingleSelectionModelImpl.java @@ -23,7 +23,6 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; -import com.vaadin.data.SelectionModel; import com.vaadin.event.selection.SingleSelectionEvent; import com.vaadin.event.selection.SingleSelectionListener; import com.vaadin.shared.Registration; @@ -32,7 +31,7 @@ import com.vaadin.shared.data.selection.SelectionServerRpc; import com.vaadin.ui.Component; import com.vaadin.ui.Grid; import com.vaadin.ui.Grid.AbstractGridExtension; -import com.vaadin.ui.Grid.GridSelectionModel; +import com.vaadin.ui.Grid.SingleSelectionModel; import com.vaadin.ui.SingleSelect; import com.vaadin.util.ReflectTools; @@ -47,8 +46,8 @@ import elemental.json.JsonObject; * @param <T> * the type of the selected item in grid. */ -public class SingleSelectionModel<T> extends AbstractGridExtension<T> - implements GridSelectionModel<T>, SelectionModel.Single<T> { +public class SingleSelectionModelImpl<T> extends AbstractGridExtension<T> + implements SingleSelectionModel<T> { private static final Method SELECTION_CHANGE_METHOD = ReflectTools .findMethod(SingleSelectionListener.class, "accept", @@ -63,7 +62,7 @@ public class SingleSelectionModel<T> extends AbstractGridExtension<T> * @param grid * the grid to bind the selection model into */ - public SingleSelectionModel(Grid<T> grid) { + public SingleSelectionModelImpl(Grid<T> grid) { this.grid = grid; extend(grid); registerRpc(new SelectionServerRpc() { @@ -83,15 +82,14 @@ public class SingleSelectionModel<T> extends AbstractGridExtension<T> } /** - * Adds a selection change listener to this select. The listener is called - * when the value of this select is changed either by the user or - * programmatically. + * Adds a selection listener to this select. The listener is called when the + * value of this select is changed either by the user or programmatically. * * @param listener * the value change listener, not null * @return a registration for the listener */ - public Registration addSelectionChangeListener( + public Registration addSelectionListener( SingleSelectionListener<T> listener) { return addListener(SingleSelectionEvent.class, listener, SELECTION_CHANGE_METHOD); @@ -148,6 +146,11 @@ public class SingleSelectionModel<T> extends AbstractGridExtension<T> * selection */ protected void doSetSelectedKey(String key) { + if (getParent() == null) { + throw new IllegalStateException( + "Trying to update selection for grid selection model that has been detached from the grid."); + } + if (selectedItem != null) { grid.getDataCommunicator().refresh(selectedItem); } @@ -233,8 +236,7 @@ public class SingleSelectionModel<T> extends AbstractGridExtension<T> // when selection model changes, firing an event for selection change // event fired before removing so that parent is still intact (in case // needed) - selectedItem = null; - fireEvent(new SingleSelectionEvent<>(grid, asSingleSelect(), false)); + setSelectedFromServer(null); super.remove(); } @@ -244,24 +246,26 @@ public class SingleSelectionModel<T> extends AbstractGridExtension<T> * * @return a single select wrapper for grid */ + @Override public SingleSelect<T> asSingleSelect() { return new SingleSelect<T>() { @Override public void setValue(T value) { - SingleSelectionModel.this.setSelectedFromServer(value); + SingleSelectionModelImpl.this.setSelectedFromServer(value); } @Override public T getValue() { - return SingleSelectionModel.this.getSelectedItem().orElse(null); + return SingleSelectionModelImpl.this.getSelectedItem() + .orElse(null); } @Override public Registration addValueChangeListener( com.vaadin.data.HasValue.ValueChangeListener<T> listener) { - return SingleSelectionModel.this.addSelectionChangeListener( - event -> listener.accept(event)); + return SingleSelectionModelImpl.this + .addSelectionListener(event -> listener.accept(event)); } @Override diff --git a/server/src/test/java/com/vaadin/data/GridAsMultiSelectInBinder.java b/server/src/test/java/com/vaadin/data/GridAsMultiSelectInBinder.java new file mode 100644 index 0000000000..e31b2ab188 --- /dev/null +++ b/server/src/test/java/com/vaadin/data/GridAsMultiSelectInBinder.java @@ -0,0 +1,270 @@ +package com.vaadin.data; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.junit.Before; +import org.junit.Test; + +import com.vaadin.data.util.converter.Converter; +import com.vaadin.data.util.converter.ValueContext; +import com.vaadin.tests.data.bean.BeanWithEnums; +import com.vaadin.tests.data.bean.Sex; +import com.vaadin.tests.data.bean.TestEnum; +import com.vaadin.ui.Grid; +import com.vaadin.ui.MultiSelect; +import com.vaadin.ui.components.grid.MultiSelectionModelImpl; +import com.vaadin.ui.components.grid.SingleSelectionModelImpl; + +public class GridAsMultiSelectInBinder + extends BinderTestBase<Binder<BeanWithEnums>, BeanWithEnums> { + public class TestEnumSetToStringConverter + implements Converter<Set<TestEnum>, String> { + @Override + public Result<String> convertToModel(Set<TestEnum> value, + ValueContext context) { + return Result.ok(value.stream().map(TestEnum::name) + .collect(Collectors.joining(","))); + } + + @Override + public Set<TestEnum> convertToPresentation(String value, + ValueContext context) { + return Stream.of(value.split(",")) + .filter(string -> !string.isEmpty()).map(TestEnum::valueOf) + .collect(Collectors.toSet()); + } + } + + private class CustomMultiSelectModel extends MultiSelectionModelImpl<Sex> { + + public CustomMultiSelectModel(Grid<Sex> grid) { + super(grid); + } + + @Override + public void updateSelection(Set<Sex> addedItems, Set<Sex> removedItems, + boolean userOriginated) { + super.updateSelection(addedItems, removedItems, userOriginated); + } + + } + + private Binder<AtomicReference<String>> converterBinder = new Binder<>(); + private Grid<TestEnum> grid; + private MultiSelect<TestEnum> select; + + @Before + public void setUp() { + binder = new Binder<>(); + item = new BeanWithEnums(); + grid = new Grid<>(); + grid.setItems(TestEnum.values()); + grid.setSelectionModel(new MultiSelectionModelImpl<>(grid)); + select = grid.asMultiSelect(); + + converterBinder.forField(select) + .withConverter(new TestEnumSetToStringConverter()) + .bind(AtomicReference::get, AtomicReference::set); + } + + @Test(expected = IllegalStateException.class) + public void boundGridInBinder_selectionModelChanged_throws() { + grid.setSelectionModel(new SingleSelectionModelImpl<>(grid)); + + select.select(TestEnum.ONE); + } + + @Test + public void beanBound_bindSelectByShortcut_selectionUpdated() { + item.setEnums(Collections.singleton(TestEnum.ONE)); + binder.setBean(item); + binder.bind(select, BeanWithEnums::getEnums, BeanWithEnums::setEnums); + + assertEquals(Collections.singleton(TestEnum.ONE), + select.getSelectedItems()); + } + + @Test + public void beanBound_bindSelect_selectionUpdated() { + item.setEnums(Collections.singleton(TestEnum.TWO)); + binder.setBean(item); + binder.forField(select).bind(BeanWithEnums::getEnums, + BeanWithEnums::setEnums); + + assertEquals(Collections.singleton(TestEnum.TWO), + select.getSelectedItems()); + } + + @Test + public void selectBound_bindBeanWithoutEnums_selectedItemNotPresent() { + bindEnum(); + + assertTrue(select.getSelectedItems().isEmpty()); + } + + @Test + public void selectBound_bindBean_selectionUpdated() { + item.setEnums(Collections.singleton(TestEnum.ONE)); + bindEnum(); + + assertEquals(Collections.singleton(TestEnum.ONE), + select.getSelectedItems()); + } + + @Test + public void bound_setSelection_beanValueUpdated() { + bindEnum(); + + select.select(TestEnum.TWO); + + assertEquals(Collections.singleton(TestEnum.TWO), item.getEnums()); + } + + @Test + public void bound_setSelection_beanValueIsACopy() { + bindEnum(); + + select.select(TestEnum.TWO); + + Set<TestEnum> enums = item.getEnums(); + + binder.setBean(new BeanWithEnums()); + select.select(TestEnum.ONE); + + assertEquals(Collections.singleton(TestEnum.TWO), enums); + } + + @Test + public void bound_deselect_beanValueUpdatedToNull() { + item.setEnums(Collections.singleton(TestEnum.ONE)); + bindEnum(); + + select.deselect(TestEnum.ONE); + + assertTrue(item.getEnums().isEmpty()); + } + + @Test + public void unbound_changeSelection_beanValueNotUpdated() { + item.setEnums(Collections.singleton(TestEnum.ONE)); + bindEnum(); + binder.removeBean(); + + select.select(TestEnum.TWO); + + assertEquals(Collections.singleton(TestEnum.ONE), item.getEnums()); + } + + @Test + public void withConverter_load_selectUpdated() { + converterBinder.readBean(new AtomicReference<>("TWO")); + + assertEquals(Collections.singleton(TestEnum.TWO), + select.getSelectedItems()); + } + + @Test + public void withConverter_save_referenceUpdated() { + select.select(TestEnum.ONE); + select.select(TestEnum.TWO); + + AtomicReference<String> reference = new AtomicReference<>(""); + converterBinder.writeBeanIfValid(reference); + + assertEquals("ONE,TWO", reference.get()); + } + + @Test + public void withValidator_validate_validatorUsed() { + binder.forField(select) + .withValidator(selection -> selection.size() % 2 == 1, + "Must select odd number of items") + .bind(BeanWithEnums::getEnums, BeanWithEnums::setEnums); + binder.setBean(item); + + assertFalse(binder.validate().isOk()); + + select.select(TestEnum.TWO); + + assertTrue(binder.validate().isOk()); + } + + @Test + public void addValueChangeListener_selectionUpdated_eventTriggeredForMultiSelect() { + Grid<Sex> grid = new Grid<>(); + CustomMultiSelectModel model = new CustomMultiSelectModel(grid); + grid.setSelectionModel(model); + grid.setItems(Sex.values()); + MultiSelect<Sex> select = grid.asMultiSelect(); + + List<Sex> selected = new ArrayList<>(); + List<Boolean> userOriginated = new ArrayList<>(); + select.addValueChangeListener(event -> { + selected.addAll(event.getValue()); + userOriginated.add(event.isUserOriginated()); + assertSame(grid, event.getComponent()); + // cannot compare that the event source is the select since a new + // MultiSelect wrapper object has been created for the event + + assertEquals(select.getValue(), event.getValue()); + }); + + select.select(Sex.UNKNOWN); + + assertEquals(Arrays.asList(Sex.UNKNOWN), selected); + + model.updateSelection(new LinkedHashSet<>(Arrays.asList(Sex.MALE)), + Collections.emptySet(), true); // simulate client side selection + assertEquals(Arrays.asList(Sex.UNKNOWN, Sex.UNKNOWN, Sex.MALE), + selected); + selected.clear(); + + select.select(Sex.MALE); // NOOP + assertEquals(Arrays.asList(), selected); + selected.clear(); + + model.updateSelection(Collections.emptySet(), + new LinkedHashSet<>(Arrays.asList(Sex.UNKNOWN)), true); // client + // side + // deselect + assertEquals(Arrays.asList(Sex.MALE), selected); + selected.clear(); + + select.deselect(Sex.UNKNOWN); // NOOP + assertEquals(Arrays.asList(), selected); + selected.clear(); + + select.deselect(Sex.FEMALE, Sex.MALE); // partly NOOP + assertEquals(Arrays.asList(), selected); + + model.selectItems(Sex.FEMALE, Sex.MALE); + assertEquals(Arrays.asList(Sex.FEMALE, Sex.MALE), selected); + selected.clear(); + + model.updateSelection(new LinkedHashSet<>(Arrays.asList(Sex.FEMALE)), + Collections.emptySet(), true); // client side NOOP + assertEquals(Arrays.asList(), selected); + + assertEquals(Arrays.asList(false, true, true, false, false), + userOriginated); + } + + protected void bindEnum() { + binder.forField(select).bind(BeanWithEnums::getEnums, + BeanWithEnums::setEnums); + binder.setBean(item); + } +} diff --git a/server/src/test/java/com/vaadin/data/GridAsSingleSelectInBinder.java b/server/src/test/java/com/vaadin/data/GridAsSingleSelectInBinder.java index 7b34c78f25..cf649384e0 100644 --- a/server/src/test/java/com/vaadin/data/GridAsSingleSelectInBinder.java +++ b/server/src/test/java/com/vaadin/data/GridAsSingleSelectInBinder.java @@ -16,7 +16,8 @@ import com.vaadin.tests.data.bean.Person; import com.vaadin.tests.data.bean.Sex; import com.vaadin.ui.Grid; import com.vaadin.ui.SingleSelect; -import com.vaadin.ui.components.grid.SingleSelectionModel; +import com.vaadin.ui.components.grid.MultiSelectionModelImpl; +import com.vaadin.ui.components.grid.SingleSelectionModelImpl; public class GridAsSingleSelectInBinder extends BinderTestBase<Binder<Person>, Person> { @@ -29,7 +30,7 @@ public class GridAsSingleSelectInBinder } } - private class CustomSingleSelectModel extends SingleSelectionModel<Sex> { + private class CustomSingleSelectModel extends SingleSelectionModelImpl<Sex> { public CustomSingleSelectModel(Grid<Sex> grid) { super(grid); @@ -52,6 +53,13 @@ public class GridAsSingleSelectInBinder select = grid.asSingleSelect(); } + @Test(expected = IllegalStateException.class) + public void boundGridInBinder_selectionModelChanged_throws() { + grid.setSelectionModel(new MultiSelectionModelImpl<>(grid)); + + select.setValue(Sex.MALE); + } + @Test public void personBound_bindSelectByShortcut_selectionUpdated() { item.setSex(Sex.FEMALE); @@ -117,8 +125,6 @@ public class GridAsSingleSelectInBinder @Test public void addValueChangeListener_selectionUpdated_eventTriggeredForSelect() { - binder = new Binder<>(); - item = new Person(); GridWithCustomSingleSelectionModel grid = new GridWithCustomSingleSelectionModel(); CustomSingleSelectModel model = new CustomSingleSelectModel(grid); grid.setSelectionModel(model); diff --git a/server/src/test/java/com/vaadin/tests/components/grid/GridMultiSelectionModelTest.java b/server/src/test/java/com/vaadin/tests/components/grid/GridMultiSelectionModelTest.java new file mode 100644 index 0000000000..d7a087be54 --- /dev/null +++ b/server/src/test/java/com/vaadin/tests/components/grid/GridMultiSelectionModelTest.java @@ -0,0 +1,543 @@ +package com.vaadin.tests.components.grid; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import org.easymock.Capture; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +import com.vaadin.data.HasValue.ValueChangeEvent; +import com.vaadin.event.selection.SingleSelectionEvent; +import com.vaadin.event.selection.SingleSelectionListener; +import com.vaadin.server.data.provider.bov.Person; +import com.vaadin.shared.Registration; +import com.vaadin.ui.Grid; +import com.vaadin.ui.Grid.GridSelectionModel; +import com.vaadin.ui.components.grid.MultiSelectionModelImpl; +import com.vaadin.ui.components.grid.SingleSelectionModelImpl; + +import elemental.json.JsonObject; + +public class GridMultiSelectionModelTest { + + public static final Person PERSON_C = new Person("c", 3); + public static final Person PERSON_B = new Person("b", 2); + public static final Person PERSON_A = new Person("a", 1); + + private Grid<Person> grid; + private MultiSelectionModelImpl<Person> selectionModel; + private Capture<List<Person>> currentSelectionCapture; + private Capture<List<Person>> oldSelectionCapture; + private AtomicInteger events; + + public static class CustomMultiSelectionModel + extends MultiSelectionModelImpl<String> { + public final Map<String, Boolean> generatedData = new LinkedHashMap<>(); + + public CustomMultiSelectionModel(Grid<String> grid) { + super(grid); + } + + @Override + public void generateData(String item, JsonObject jsonObject) { + super.generateData(item, jsonObject); + // capture updated row + generatedData.put(item, isSelected(item)); + } + + } + + @Before + public void setUp() { + grid = new Grid<>(); + selectionModel = new MultiSelectionModelImpl<>(grid); + grid.setSelectionModel(selectionModel); + grid.setItems(PERSON_A, PERSON_B, PERSON_C); + + currentSelectionCapture = new Capture<>(); + oldSelectionCapture = new Capture<>(); + events = new AtomicInteger(); + + selectionModel.addSelectionListener(event -> { + currentSelectionCapture + .setValue(new ArrayList<>(event.getNewSelection())); + oldSelectionCapture + .setValue(new ArrayList<>(event.getOldSelection())); + events.incrementAndGet(); + }); + } + + @Test(expected = IllegalStateException.class) + public void selectionModelChanged_usingPreviousSelectionModel_throws() { + grid.setSelectionModel(new SingleSelectionModelImpl<>(grid)); + + selectionModel.select(PERSON_A); + } + + @Test + public void changingSelectionModel_firesSelectionEvent() { + Grid<String> customGrid = new Grid<>(); + customGrid.setSelectionModel(new MultiSelectionModelImpl<>(customGrid)); + customGrid.setItems("Foo", "Bar", "Baz"); + + List<String> selectionChanges = new ArrayList<>(); + Capture<List<String>> oldSelectionCapture = new Capture<>(); + ((MultiSelectionModelImpl<String>) customGrid.getSelectionModel()) + .addSelectionListener(e -> { + selectionChanges.addAll(e.getValue()); + oldSelectionCapture + .setValue(new ArrayList<>(e.getOldSelection())); + }); + + customGrid.getSelectionModel().select("Foo"); + assertEquals(Arrays.asList("Foo"), selectionChanges); + selectionChanges.clear(); + + customGrid.getSelectionModel().select("Bar"); + assertEquals("Foo", + customGrid.getSelectionModel().getFirstSelectedItem().get()); + assertEquals(Arrays.asList("Foo", "Bar"), selectionChanges); + selectionChanges.clear(); + + customGrid.setSelectionModel(new SingleSelectionModelImpl<>(customGrid)); + assertFalse(customGrid.getSelectionModel().getFirstSelectedItem() + .isPresent()); + assertEquals(Arrays.asList(), selectionChanges); + assertEquals(Arrays.asList("Foo", "Bar"), + oldSelectionCapture.getValue()); + } + + @Test + public void serverSideSelection_GridChangingSelectionModel_sendsUpdatedRowsToClient() { + Grid<String> customGrid = new Grid<>(); + customGrid.setItems("Foo", "Bar", "Baz"); + + CustomMultiSelectionModel customModel = new CustomMultiSelectionModel( + customGrid); + customGrid.setSelectionModel(customModel); + customGrid.getDataCommunicator().beforeClientResponse(true); + + Assert.assertFalse("Item should have been updated as selected", + customModel.generatedData.get("Foo")); + Assert.assertFalse("Item should have been updated as NOT selected", + customModel.generatedData.get("Bar")); + Assert.assertFalse("Item should have been updated as NOT selected", + customModel.generatedData.get("Baz")); + + customModel.generatedData.clear(); + + customGrid.getSelectionModel().select("Foo"); + customGrid.getDataCommunicator().beforeClientResponse(false); + + Assert.assertTrue("Item should have been updated as selected", + customModel.generatedData.get("Foo")); + Assert.assertFalse("Item should have NOT been updated", + customModel.generatedData.containsKey("Bar")); + Assert.assertFalse("Item should have NOT been updated", + customModel.generatedData.containsKey("Baz")); + + customModel.generatedData.clear(); + + customModel.updateSelection(asSet("Bar"), asSet("Foo")); + customGrid.getDataCommunicator().beforeClientResponse(false); + + Assert.assertFalse("Item should have been updated as NOT selected", + customModel.generatedData.get("Foo")); + Assert.assertTrue("Item should have been updated as selected", + customModel.generatedData.get("Bar")); + Assert.assertFalse("Item should have NOT been updated", + customModel.generatedData.containsKey("Baz")); + + // switch to single to cause event + customModel.generatedData.clear(); + customGrid.setSelectionModel(new SingleSelectionModelImpl<>(customGrid)); + customGrid.getDataCommunicator().beforeClientResponse(false); + + // changing selection model should trigger row updates, but the old + // selection model is not triggered as it has been removed + Assert.assertTrue(customModel.generatedData.isEmpty()); // not triggered + } + + @Test + public void select_gridWithStrings() { + Grid<String> gridWithStrings = new Grid<>(); + gridWithStrings + .setSelectionModel(new MultiSelectionModelImpl<>(gridWithStrings)); + gridWithStrings.setItems("Foo", "Bar", "Baz"); + + GridSelectionModel<String> model = gridWithStrings.getSelectionModel(); + Assert.assertFalse(model.isSelected("Foo")); + + model.select("Foo"); + Assert.assertTrue(model.isSelected("Foo")); + Assert.assertEquals(Optional.of("Foo"), model.getFirstSelectedItem()); + + model.select("Bar"); + Assert.assertTrue(model.isSelected("Foo")); + Assert.assertTrue(model.isSelected("Bar")); + Assert.assertEquals(Arrays.asList("Foo", "Bar"), + new ArrayList<>(model.getSelectedItems())); + + model.deselect("Bar"); + Assert.assertFalse(model.isSelected("Bar")); + Assert.assertTrue(model.getFirstSelectedItem().isPresent()); + Assert.assertEquals(Arrays.asList("Foo"), + new ArrayList<>(model.getSelectedItems())); + } + + @Test + public void select() { + selectionModel.select(PERSON_B); + + assertEquals(PERSON_B, + selectionModel.getFirstSelectedItem().orElse(null)); + assertEquals(Optional.of(PERSON_B), + selectionModel.getFirstSelectedItem()); + + assertFalse(selectionModel.isSelected(PERSON_A)); + assertTrue(selectionModel.isSelected(PERSON_B)); + assertFalse(selectionModel.isSelected(PERSON_C)); + + assertEquals(Arrays.asList(PERSON_B), + currentSelectionCapture.getValue()); + + selectionModel.select(PERSON_A); + assertEquals(PERSON_B, + selectionModel.getFirstSelectedItem().orElse(null)); + + assertTrue(selectionModel.isSelected(PERSON_A)); + assertTrue(selectionModel.isSelected(PERSON_B)); + assertFalse(selectionModel.isSelected(PERSON_C)); + + assertEquals(Arrays.asList(PERSON_B, PERSON_A), + currentSelectionCapture.getValue()); + assertEquals(2, events.get()); + } + + @Test + public void deselect() { + selectionModel.select(PERSON_B); + selectionModel.deselect(PERSON_B); + + assertFalse(selectionModel.getFirstSelectedItem().isPresent()); + + assertFalse(selectionModel.isSelected(PERSON_A)); + assertFalse(selectionModel.isSelected(PERSON_B)); + assertFalse(selectionModel.isSelected(PERSON_C)); + + assertEquals(Arrays.asList(), currentSelectionCapture.getValue()); + assertEquals(2, events.get()); + } + + @Test + public void selectItems() { + selectionModel.selectItems(PERSON_C, PERSON_B); + + assertEquals(PERSON_C, + selectionModel.getFirstSelectedItem().orElse(null)); + assertEquals(Optional.of(PERSON_C), + selectionModel.getFirstSelectedItem()); + + assertFalse(selectionModel.isSelected(PERSON_A)); + assertTrue(selectionModel.isSelected(PERSON_B)); + assertTrue(selectionModel.isSelected(PERSON_C)); + + assertEquals(Arrays.asList(PERSON_C, PERSON_B), + currentSelectionCapture.getValue()); + + selectionModel.selectItems(PERSON_A, PERSON_C); // partly NOOP + assertTrue(selectionModel.isSelected(PERSON_A)); + assertTrue(selectionModel.isSelected(PERSON_B)); + assertTrue(selectionModel.isSelected(PERSON_C)); + + assertEquals(Arrays.asList(PERSON_C, PERSON_B, PERSON_A), + currentSelectionCapture.getValue()); + assertEquals(2, events.get()); + } + + @Test + public void deselectItems() { + selectionModel.selectItems(PERSON_C, PERSON_A, PERSON_B); + + selectionModel.deselectItems(PERSON_A); + assertEquals(PERSON_C, + selectionModel.getFirstSelectedItem().orElse(null)); + assertEquals(Optional.of(PERSON_C), + selectionModel.getFirstSelectedItem()); + + assertFalse(selectionModel.isSelected(PERSON_A)); + assertTrue(selectionModel.isSelected(PERSON_B)); + assertTrue(selectionModel.isSelected(PERSON_C)); + + assertEquals(Arrays.asList(PERSON_C, PERSON_B), + currentSelectionCapture.getValue()); + + selectionModel.deselectItems(PERSON_A, PERSON_B, PERSON_C); + assertNull(selectionModel.getFirstSelectedItem().orElse(null)); + assertEquals(Optional.empty(), selectionModel.getFirstSelectedItem()); + + assertFalse(selectionModel.isSelected(PERSON_A)); + assertFalse(selectionModel.isSelected(PERSON_B)); + assertFalse(selectionModel.isSelected(PERSON_C)); + + assertEquals(Arrays.asList(), currentSelectionCapture.getValue()); + assertEquals(3, events.get()); + } + + @Test + public void selectionEvent_newSelection_oldSelection() { + selectionModel.selectItems(PERSON_C, PERSON_A, PERSON_B); + + assertEquals(Arrays.asList(PERSON_C, PERSON_A, PERSON_B), + currentSelectionCapture.getValue()); + assertEquals(Arrays.asList(), oldSelectionCapture.getValue()); + + selectionModel.deselect(PERSON_A); + + assertEquals(Arrays.asList(PERSON_C, PERSON_B), + currentSelectionCapture.getValue()); + assertEquals(Arrays.asList(PERSON_C, PERSON_A, PERSON_B), + oldSelectionCapture.getValue()); + + selectionModel.deselectItems(PERSON_A, PERSON_B, PERSON_C); + assertEquals(Arrays.asList(), currentSelectionCapture.getValue()); + assertEquals(Arrays.asList(PERSON_C, PERSON_B), + oldSelectionCapture.getValue()); + + selectionModel.selectItems(PERSON_A); + assertEquals(Arrays.asList(PERSON_A), + currentSelectionCapture.getValue()); + assertEquals(Arrays.asList(), oldSelectionCapture.getValue()); + + selectionModel.updateSelection( + new LinkedHashSet<>(Arrays.asList(PERSON_B, PERSON_C)), + new LinkedHashSet<>(Arrays.asList(PERSON_A))); + assertEquals(Arrays.asList(PERSON_B, PERSON_C), + currentSelectionCapture.getValue()); + assertEquals(Arrays.asList(PERSON_A), oldSelectionCapture.getValue()); + + selectionModel.deselectAll(); + assertEquals(Arrays.asList(), currentSelectionCapture.getValue()); + assertEquals(Arrays.asList(PERSON_B, PERSON_C), + oldSelectionCapture.getValue()); + + selectionModel.select(PERSON_C); + assertEquals(Arrays.asList(PERSON_C), + currentSelectionCapture.getValue()); + assertEquals(Arrays.asList(), oldSelectionCapture.getValue()); + + selectionModel.deselect(PERSON_C); + assertEquals(Arrays.asList(), currentSelectionCapture.getValue()); + assertEquals(Arrays.asList(PERSON_C), oldSelectionCapture.getValue()); + } + + @Test + public void deselectAll() { + selectionModel.selectItems(PERSON_A, PERSON_C, PERSON_B); + + assertTrue(selectionModel.isSelected(PERSON_A)); + assertTrue(selectionModel.isSelected(PERSON_B)); + assertTrue(selectionModel.isSelected(PERSON_C)); + assertEquals(Arrays.asList(PERSON_A, PERSON_C, PERSON_B), + currentSelectionCapture.getValue()); + assertEquals(1, events.get()); + + selectionModel.deselectAll(); + assertFalse(selectionModel.isSelected(PERSON_A)); + assertFalse(selectionModel.isSelected(PERSON_B)); + assertFalse(selectionModel.isSelected(PERSON_C)); + assertEquals(Arrays.asList(), currentSelectionCapture.getValue()); + assertEquals(Arrays.asList(PERSON_A, PERSON_C, PERSON_B), + oldSelectionCapture.getValue()); + assertEquals(2, events.get()); + + selectionModel.select(PERSON_C); + assertFalse(selectionModel.isSelected(PERSON_A)); + assertFalse(selectionModel.isSelected(PERSON_B)); + assertTrue(selectionModel.isSelected(PERSON_C)); + assertEquals(Arrays.asList(PERSON_C), + currentSelectionCapture.getValue()); + assertEquals(Arrays.asList(), oldSelectionCapture.getValue()); + assertEquals(3, events.get()); + + selectionModel.deselectAll(); + assertFalse(selectionModel.isSelected(PERSON_A)); + assertFalse(selectionModel.isSelected(PERSON_B)); + assertFalse(selectionModel.isSelected(PERSON_C)); + assertEquals(Arrays.asList(), currentSelectionCapture.getValue()); + assertEquals(Arrays.asList(PERSON_C), oldSelectionCapture.getValue()); + assertEquals(4, events.get()); + + selectionModel.deselectAll(); + assertEquals(4, events.get()); + } + + @Test + public void updateSelection() { + selectionModel.updateSelection(asSet(PERSON_A), Collections.emptySet()); + + assertTrue(selectionModel.isSelected(PERSON_A)); + assertFalse(selectionModel.isSelected(PERSON_B)); + assertFalse(selectionModel.isSelected(PERSON_C)); + assertEquals(Arrays.asList(PERSON_A), + currentSelectionCapture.getValue()); + assertEquals(1, events.get()); + + selectionModel.updateSelection(asSet(PERSON_B), asSet(PERSON_A)); + + assertFalse(selectionModel.isSelected(PERSON_A)); + assertTrue(selectionModel.isSelected(PERSON_B)); + assertFalse(selectionModel.isSelected(PERSON_C)); + assertEquals(Arrays.asList(PERSON_B), + currentSelectionCapture.getValue()); + assertEquals(Arrays.asList(PERSON_A), oldSelectionCapture.getValue()); + assertEquals(2, events.get()); + + selectionModel.updateSelection(asSet(PERSON_B), asSet(PERSON_A)); // NOOP + + assertFalse(selectionModel.isSelected(PERSON_A)); + assertTrue(selectionModel.isSelected(PERSON_B)); + assertFalse(selectionModel.isSelected(PERSON_C)); + assertEquals(Arrays.asList(PERSON_B), + currentSelectionCapture.getValue()); + assertEquals(Arrays.asList(PERSON_A), oldSelectionCapture.getValue()); + assertEquals(2, events.get()); + + selectionModel.updateSelection(asSet(PERSON_A, PERSON_C), + asSet(PERSON_A)); // partly NOOP + + assertFalse(selectionModel.isSelected(PERSON_A)); + assertTrue(selectionModel.isSelected(PERSON_B)); + assertTrue(selectionModel.isSelected(PERSON_C)); + assertEquals(Arrays.asList(PERSON_B, PERSON_C), + currentSelectionCapture.getValue()); + assertEquals(Arrays.asList(PERSON_B), oldSelectionCapture.getValue()); + assertEquals(3, events.get()); + + selectionModel.updateSelection(asSet(PERSON_B, PERSON_A), + asSet(PERSON_B)); // partly NOOP + + assertTrue(selectionModel.isSelected(PERSON_A)); + assertTrue(selectionModel.isSelected(PERSON_B)); + assertTrue(selectionModel.isSelected(PERSON_C)); + assertEquals(Arrays.asList(PERSON_B, PERSON_C, PERSON_A), + currentSelectionCapture.getValue()); + assertEquals(Arrays.asList(PERSON_B, PERSON_C), + oldSelectionCapture.getValue()); + assertEquals(4, events.get()); + + selectionModel.updateSelection(asSet(), + asSet(PERSON_B, PERSON_A, PERSON_C)); + + assertFalse(selectionModel.isSelected(PERSON_A)); + assertFalse(selectionModel.isSelected(PERSON_B)); + assertFalse(selectionModel.isSelected(PERSON_C)); + assertEquals(Arrays.asList(), currentSelectionCapture.getValue()); + assertEquals(Arrays.asList(PERSON_B, PERSON_C, PERSON_A), + oldSelectionCapture.getValue()); + assertEquals(5, events.get()); + } + + private <T> Set<T> asSet(@SuppressWarnings("unchecked") T... people) { + return new LinkedHashSet<>(Arrays.asList(people)); + } + + @Test + public void selectTwice() { + selectionModel.select(PERSON_C); + selectionModel.select(PERSON_C); + + assertEquals(PERSON_C, + selectionModel.getFirstSelectedItem().orElse(null)); + + assertFalse(selectionModel.isSelected(PERSON_A)); + assertFalse(selectionModel.isSelected(PERSON_B)); + assertTrue(selectionModel.isSelected(PERSON_C)); + + assertEquals(Optional.of(PERSON_C), + selectionModel.getFirstSelectedItem()); + + assertEquals(Arrays.asList(PERSON_C), + currentSelectionCapture.getValue()); + assertEquals(1, events.get()); + } + + @Test + public void deselectTwice() { + selectionModel.select(PERSON_C); + assertEquals(Arrays.asList(PERSON_C), + currentSelectionCapture.getValue()); + assertEquals(1, events.get()); + + selectionModel.deselect(PERSON_C); + + assertFalse(selectionModel.getFirstSelectedItem().isPresent()); + assertFalse(selectionModel.isSelected(PERSON_C)); + assertEquals(Arrays.asList(), currentSelectionCapture.getValue()); + assertEquals(2, events.get()); + + selectionModel.deselect(PERSON_C); + + assertFalse(selectionModel.getFirstSelectedItem().isPresent()); + assertFalse(selectionModel.isSelected(PERSON_A)); + assertFalse(selectionModel.isSelected(PERSON_B)); + assertFalse(selectionModel.isSelected(PERSON_C)); + assertEquals(Arrays.asList(), currentSelectionCapture.getValue()); + assertEquals(2, events.get()); + } + + @SuppressWarnings({ "serial" }) + @Test + public void addValueChangeListener() { + AtomicReference<SingleSelectionListener<String>> selectionListener = new AtomicReference<>(); + Registration registration = Mockito.mock(Registration.class); + Grid<String> grid = new Grid<>(); + grid.setItems("foo", "bar"); + String value = "foo"; + SingleSelectionModelImpl<String> select = new SingleSelectionModelImpl<String>( + grid) { + @Override + public Registration addSelectionListener( + SingleSelectionListener<String> listener) { + selectionListener.set(listener); + return registration; + } + + @Override + public Optional<String> getSelectedItem() { + return Optional.of(value); + } + }; + + AtomicReference<ValueChangeEvent<?>> event = new AtomicReference<>(); + Registration actualRegistration = select.addSelectionListener(evt -> { + Assert.assertNull(event.get()); + event.set(evt); + }); + Assert.assertSame(registration, actualRegistration); + + selectionListener.get().accept(new SingleSelectionEvent<>(grid, + select.asSingleSelect(), true)); + + Assert.assertEquals(grid, event.get().getComponent()); + Assert.assertEquals(value, event.get().getValue()); + Assert.assertTrue(event.get().isUserOriginated()); + } +} diff --git a/server/src/test/java/com/vaadin/tests/components/grid/GridSingleSelectionModelTest.java b/server/src/test/java/com/vaadin/tests/components/grid/GridSingleSelectionModelTest.java index 67b35c5208..17b409c1a6 100644 --- a/server/src/test/java/com/vaadin/tests/components/grid/GridSingleSelectionModelTest.java +++ b/server/src/test/java/com/vaadin/tests/components/grid/GridSingleSelectionModelTest.java @@ -6,7 +6,9 @@ import static org.junit.Assert.assertTrue; import java.util.ArrayList; import java.util.Arrays; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.concurrent.atomic.AtomicReference; @@ -20,62 +22,124 @@ import com.vaadin.event.selection.SingleSelectionEvent; import com.vaadin.event.selection.SingleSelectionListener; import com.vaadin.server.data.provider.bov.Person; import com.vaadin.shared.Registration; -import com.vaadin.shared.data.DataCommunicatorClientRpc; import com.vaadin.ui.Grid; import com.vaadin.ui.Grid.GridSelectionModel; -import com.vaadin.ui.components.grid.SingleSelectionModel; +import com.vaadin.ui.components.grid.MultiSelectionModelImpl; +import com.vaadin.ui.components.grid.SingleSelectionModelImpl; + +import elemental.json.JsonObject; public class GridSingleSelectionModelTest { public static final Person PERSON_C = new Person("c", 3); public static final Person PERSON_B = new Person("b", 2); public static final Person PERSON_A = new Person("a", 1); - public static final String RPC_INTERFACE = DataCommunicatorClientRpc.class - .getName(); - private class CustomSelectionModelGrid extends Grid<String> { - public void switchSelectionModel() { - // just switch selection model to cause event - setSelectionModel(new SingleSelectionModel(this)); + public static class CustomSingleSelectionModel + extends SingleSelectionModelImpl<String> { + public final Map<String, Boolean> generatedData = new LinkedHashMap<>(); + + public CustomSingleSelectionModel(Grid<String> grid) { + super(grid); } + + @Override + public void generateData(String item, JsonObject jsonObject) { + super.generateData(item, jsonObject); + // capture updated row + generatedData.put(item, isSelected(item)); + } + } private List<Person> selectionChanges; private Grid<Person> grid; - private SingleSelectionModel<Person> selectionModel; + private SingleSelectionModelImpl<Person> selectionModel; @Before public void setUp() { grid = new Grid<>(); grid.setItems(PERSON_A, PERSON_B, PERSON_C); - selectionModel = (SingleSelectionModel<Person>) grid + selectionModel = (SingleSelectionModelImpl<Person>) grid .getSelectionModel(); selectionChanges = new ArrayList<>(); - selectionModel.addSelectionChangeListener( - e -> selectionChanges.add(e.getValue())); + selectionModel + .addSelectionListener(e -> selectionChanges.add(e.getValue())); + } + + @Test(expected = IllegalStateException.class) + public void selectionModelChanged_usingPreviousSelectionModel_throws() { + grid.setSelectionModel(new MultiSelectionModelImpl<>(grid)); + + selectionModel.select(PERSON_A); } @Test - public void testGridChangingSelectionModel_firesSelectionChangeEvent() { - CustomSelectionModelGrid customGrid = new CustomSelectionModelGrid(); + public void gridChangingSelectionModel_firesSelectionChangeEvent() { + Grid<String> customGrid = new Grid<>(); customGrid.setItems("Foo", "Bar", "Baz"); List<String> selectionChanges = new ArrayList<>(); - ((SingleSelectionModel<String>) customGrid.getSelectionModel()) - .addSelectionChangeListener( - e -> selectionChanges.add(e.getValue())); + ((SingleSelectionModelImpl<String>) customGrid.getSelectionModel()) + .addSelectionListener(e -> selectionChanges.add(e.getValue())); customGrid.getSelectionModel().select("Foo"); assertEquals("Foo", customGrid.getSelectionModel().getFirstSelectedItem().get()); assertEquals(Arrays.asList("Foo"), selectionChanges); - customGrid.switchSelectionModel(); + customGrid + .setSelectionModel(new CustomSingleSelectionModel(customGrid)); assertEquals(Arrays.asList("Foo", null), selectionChanges); } @Test + public void serverSideSelection_GridChangingSelectionModel_sendsUpdatedRowsToClient() { + Grid<String> customGrid = new Grid<>(); + customGrid.setItems("Foo", "Bar", "Baz"); + + CustomSingleSelectionModel customModel = new CustomSingleSelectionModel( + customGrid); + customGrid.setSelectionModel(customModel); + + customGrid.getDataCommunicator().beforeClientResponse(true); + + Assert.assertFalse("Item should have been updated as selected", + customModel.generatedData.get("Foo")); + Assert.assertFalse("Item should have been updated as NOT selected", + customModel.generatedData.get("Bar")); + Assert.assertFalse("Item should have been updated as NOT selected", + customModel.generatedData.get("Baz")); + + customModel.generatedData.clear(); + + customGrid.getSelectionModel().select("Foo"); + customGrid.getDataCommunicator().beforeClientResponse(false); + + Assert.assertTrue("Item should have been updated as selected", + customModel.generatedData.get("Foo")); + Assert.assertFalse("Item should have NOT been updated", + customModel.generatedData.containsKey("Bar")); + Assert.assertFalse("Item should have NOT been updated", + customModel.generatedData.containsKey("Baz")); + + // switch to another selection model to cause event + customModel.generatedData.clear(); + customGrid.setSelectionModel(new SingleSelectionModelImpl<>(customGrid)); + customGrid.getDataCommunicator().beforeClientResponse(false); + + // since the selection model has been removed, it is no longer a data + // generator for the data communicator, would need to verify somehow + // that row is not marked as selected anymore ? (done in UI tests) + Assert.assertTrue(customModel.generatedData.isEmpty()); // at least + // removed + // selection + // model is not + // triggered + } + + @Test public void testGridWithSingleSelection() { Grid<String> gridWithStrings = new Grid<>(); gridWithStrings.setItems("Foo", "Bar", "Baz"); @@ -210,10 +274,10 @@ public class GridSingleSelectionModelTest { Grid<String> grid = new Grid<>(); grid.setItems("foo", "bar"); String value = "foo"; - SingleSelectionModel<String> select = new SingleSelectionModel<String>( + SingleSelectionModelImpl<String> select = new SingleSelectionModelImpl<String>( grid) { @Override - public Registration addSelectionChangeListener( + public Registration addSelectionListener( SingleSelectionListener<String> listener) { selectionListener.set(listener); return registration; @@ -226,11 +290,10 @@ public class GridSingleSelectionModelTest { }; AtomicReference<ValueChangeEvent<?>> event = new AtomicReference<>(); - Registration actualRegistration = select - .addSelectionChangeListener(evt -> { - Assert.assertNull(event.get()); - event.set(evt); - }); + Registration actualRegistration = select.addSelectionListener(evt -> { + Assert.assertNull(event.get()); + event.set(evt); + }); Assert.assertSame(registration, actualRegistration); selectionListener.get().accept(new SingleSelectionEvent<>(grid, |