From f2d8f812efa067b4baa7e27c0ea76f7596b291e6 Mon Sep 17 00:00:00 2001 From: Pekka Hyvönen Date: Fri, 11 Nov 2016 09:41:43 +0200 Subject: 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 --- .../main/java/com/vaadin/data/SelectionModel.java | 27 +- .../event/selection/MultiSelectionEvent.java | 33 +- server/src/main/java/com/vaadin/ui/Grid.java | 75 ++- .../components/grid/MultiSelectionModelImpl.java | 277 +++++++++++ .../ui/components/grid/SingleSelectionModel.java | 295 ----------- .../components/grid/SingleSelectionModelImpl.java | 299 ++++++++++++ .../com/vaadin/data/GridAsMultiSelectInBinder.java | 270 ++++++++++ .../vaadin/data/GridAsSingleSelectInBinder.java | 14 +- .../grid/GridMultiSelectionModelTest.java | 543 +++++++++++++++++++++ .../grid/GridSingleSelectionModelTest.java | 113 ++++- 10 files changed, 1609 insertions(+), 337 deletions(-) create mode 100644 server/src/main/java/com/vaadin/ui/components/grid/MultiSelectionModelImpl.java delete mode 100644 server/src/main/java/com/vaadin/ui/components/grid/SingleSelectionModel.java create mode 100644 server/src/main/java/com/vaadin/ui/components/grid/SingleSelectionModelImpl.java create mode 100644 server/src/test/java/com/vaadin/data/GridAsMultiSelectInBinder.java create mode 100644 server/src/test/java/com/vaadin/tests/components/grid/GridMultiSelectionModelTest.java (limited to 'server/src') 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 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 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. *

@@ -183,6 +194,14 @@ public interface SelectionModel extends Serializable { default Optional 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 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 @@ -55,13 +57,32 @@ public class MultiSelectionEvent extends ValueChangeEvent> this.oldSelection = oldSelection; } + /** + * 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 source, + Set oldSelection, boolean userOriginated) { + super(component, source, userOriginated); + this.oldSelection = oldSelection; + } + /** * Gets the new selection. *

* 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 extends ValueChangeEvent> public Optional 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 getSource() { + return (MultiSelect) 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 extends AbstractListing /** * The server-side interface that controls Grid's selection state. * SelectionModel should extend {@link AbstractGridExtension}. + *

* * @param * the grid bean type + * @see SingleSelectionModel + * @see MultiSelectionModel */ public interface GridSelectionModel extends SelectionModel, Extension { @@ -151,6 +154,42 @@ public class Grid extends AbstractListing void remove(); } + /** + * Single selection model interface for Grid. + * + * @param + * the type of items in grid + */ + public interface SingleSelectionModel extends GridSelectionModel, + com.vaadin.data.SelectionModel.Single { + + /** + * Gets a wrapper to use this single selection model as a single select + * in {@link Binder}. + * + * @return the single select wrapper + */ + SingleSelect asSingleSelect(); + } + + /** + * Multiselection model interface for Grid. + * + * @param + * the type of items in grid + */ + public interface MultiSelectionModel extends GridSelectionModel, + com.vaadin.data.SelectionModel.Multi { + + /** + * Gets a wrapper to use this multiselection model as a multiselect in + * {@link Binder}. + * + * @return the multiselect wrapper + */ + MultiSelect asMultiSelect(); + } + /** * An event listener for column resize events in the Grid. */ @@ -2030,7 +2069,7 @@ public class Grid extends AbstractListing setDefaultHeaderRow(appendHeaderRow()); - selectionModel = new SingleSelectionModel<>(this); + selectionModel = new SingleSelectionModelImpl<>(this); detailsManager = new DetailsManager<>(); addExtension(detailsManager); @@ -2896,15 +2935,18 @@ public class Grid extends AbstractListing /** * Use this grid as a single select in {@link Binder}. *

- * 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 asSingleSelect() { GridSelectionModel 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) model).asSingleSelect(); @@ -2914,15 +2956,34 @@ public class Grid extends AbstractListing return editor; } + /** + * User this grid as a multiselect in {@link Binder}. + *

+ * 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 asMultiSelect() { + GridSelectionModel 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) model).asMultiSelect(); + } + /** * Sets the selection model for this listing. *

- * 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 model) { + public void setSelectionModel(GridSelectionModel 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. + *

+ * 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 + * the type of the selected item in grid. + */ +public class MultiSelectionModelImpl extends AbstractGridExtension + implements MultiSelectionModel { + + 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 grid; + + private Set 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 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 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 getSelectedItems() { + return Collections.unmodifiableSet(new LinkedHashSet<>(selection)); + } + + @Override + public void updateSelection(Set addedItems, Set 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 asMultiSelect() { + return new MultiSelect() { + + @Override + public void setValue(Set value) { + Objects.requireNonNull(value); + Set copy = value.stream().map(Objects::requireNonNull) + .collect(Collectors.toCollection(LinkedHashSet::new)); + + updateSelection(copy, new LinkedHashSet<>(getSelectedItems())); + } + + @Override + public Set getValue() { + return getSelectedItems(); + } + + @Override + public Registration addValueChangeListener( + com.vaadin.data.HasValue.ValueChangeListener> 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 addedItems, + Set removedItems) { + MultiSelectionModelImpl.this.updateSelection(addedItems, + removedItems); + } + + @Override + public Set getSelectedItems() { + return MultiSelectionModelImpl.this.getSelectedItems(); + } + + @Override + public Registration addSelectionListener( + MultiSelectionListener listener) { + return MultiSelectionModelImpl.this + .addSelectionListener(listener); + } + }; + } + + /** + * Updates the selection by adding and removing the given items. + *

+ * 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 addedItems, Set 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> 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 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/SingleSelectionModel.java deleted file mode 100644 index d095504768..0000000000 --- a/server/src/main/java/com/vaadin/ui/components/grid/SingleSelectionModel.java +++ /dev/null @@ -1,295 +0,0 @@ -/* - * 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.HashSet; -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; -import com.vaadin.shared.data.DataCommunicatorConstants; -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.SingleSelect; -import com.vaadin.util.ReflectTools; - -import elemental.json.JsonObject; - -/** - * Single selection model for grid. - * - * @author Vaadin Ltd. - * @since 8.0 - * - * @param - * the type of the selected item in grid. - */ -public class SingleSelectionModel extends AbstractGridExtension - implements GridSelectionModel, SelectionModel.Single { - - private static final Method SELECTION_CHANGE_METHOD = ReflectTools - .findMethod(SingleSelectionListener.class, "accept", - SingleSelectionEvent.class); - - private final Grid grid; - private T selectedItem = null; - - /** - * Constructs a new single selection model for the given grid. - * - * @param grid - * the grid to bind the selection model into - */ - public SingleSelectionModel(Grid grid) { - this.grid = grid; - extend(grid); - registerRpc(new SelectionServerRpc() { - - @Override - public void select(String key) { - setSelectedFromClient(key); - } - - @Override - public void deselect(String key) { - if (isKeySelected(key)) { - setSelectedFromClient(null); - } - } - }); - } - - /** - * 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. - * - * @param listener - * the value change listener, not null - * @return a registration for the listener - */ - public Registration addSelectionChangeListener( - SingleSelectionListener listener) { - return addListener(SingleSelectionEvent.class, listener, - SELECTION_CHANGE_METHOD); - } - - @Override - public Optional getSelectedItem() { - return Optional.ofNullable(selectedItem); - } - - @Override - public void deselect(T item) { - Objects.requireNonNull(item, "deselected item cannot be null"); - if (isSelected(item)) { - setSelectedFromServer(null); - } - } - - @Override - public void select(T item) { - Objects.requireNonNull(item, "selected item cannot be null"); - setSelectedFromServer(item); - } - - /** - * Returns whether the given key maps to the currently selected item. - * - * @param key - * the key to test or {@code null} to test whether nothing is - * selected - * @return {@code true} if the key equals the key of the currently selected - * item (or {@code null} if no selection), {@code false} otherwise. - */ - protected boolean isKeySelected(String key) { - return Objects.equals(key, getSelectedKey()); - } - - /** - * Returns the communication key of the selected item or {@code null} if no - * item is selected. - * - * @return the key of the selected item if any, {@code null} otherwise. - */ - protected String getSelectedKey() { - return itemToKey(selectedItem); - } - - /** - * Sets the selected item based on the given communication key. If the key - * is {@code null}, clears the current selection if any. - * - * @param key - * the key of the selected item or {@code null} to clear - * selection - */ - protected void doSetSelectedKey(String key) { - if (selectedItem != null) { - grid.getDataCommunicator().refresh(selectedItem); - } - selectedItem = getData(key); - if (selectedItem != null) { - grid.getDataCommunicator().refresh(selectedItem); - } - } - - /** - * Sets the selection based on a client request. Does nothing if the select - * component is {@linkplain Component#isReadOnly()} or if the selection - * would not change. Otherwise updates the selection and fires a selection - * change event with {@code isUserOriginated == true}. - * - * @param key - * the key of the item to select or {@code null} to clear - * selection - */ - protected void setSelectedFromClient(String key) { - if (isKeySelected(key)) { - return; - } - - doSetSelectedKey(key); - fireEvent(new SingleSelectionEvent<>(grid, asSingleSelect(), true)); - } - - /** - * Sets the selection based on server API call. Does nothing if the - * selection would not change; otherwise updates the selection and fires a - * selection change event with {@code isUserOriginated == false}. - * - * @param item - * the item to select or {@code null} to clear selection - */ - protected void setSelectedFromServer(T item) { - // TODO creates a key if item not in data provider - String key = itemToKey(item); - - if (isSelected(item) || isKeySelected(key)) { - return; - } - - doSetSelectedKey(key); - fireEvent(new SingleSelectionEvent<>(grid, asSingleSelect(), false)); - } - - /** - * Returns the communication key assigned to the given item. - * - * @param item - * the item whose key to return - * @return the assigned key - */ - protected String itemToKey(T item) { - if (item == null) { - return null; - } else { - // TODO creates a key if item not in data provider - return grid.getDataCommunicator().getKeyMapper().key(item); - } - } - - @Override - public Set getSelectedItems() { - if (selectedItem != null) { - return new HashSet<>(Arrays.asList(selectedItem)); - } else { - return Collections.emptySet(); - } - } - - @Override - public void generateData(T item, JsonObject jsonObject) { - if (isSelected(item)) { - jsonObject.put(DataCommunicatorConstants.SELECTED, true); - } - } - - @Override - public void remove() { - // 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)); - - super.remove(); - } - - /** - * Gets a wrapper for using this grid as a single select in a binder. - * - * @return a single select wrapper for grid - */ - public SingleSelect asSingleSelect() { - return new SingleSelect() { - - @Override - public void setValue(T value) { - SingleSelectionModel.this.setSelectedFromServer(value); - } - - @Override - public T getValue() { - return SingleSelectionModel.this.getSelectedItem().orElse(null); - } - - @Override - public Registration addValueChangeListener( - com.vaadin.data.HasValue.ValueChangeListener listener) { - return SingleSelectionModel.this.addSelectionChangeListener( - event -> listener.accept(event)); - } - - @Override - public void setRequiredIndicatorVisible( - boolean requiredIndicatorVisible) { - // TODO support required indicator when grid is used in binder ? - throw new UnsupportedOperationException( - "Required indicator is not supported for Grid."); - } - - @Override - public boolean isRequiredIndicatorVisible() { - throw new UnsupportedOperationException( - "Required indicator is not supported for Grid."); - } - - @Override - public void setReadOnly(boolean readOnly) { - // TODO support read only when grid is used in binder ? - throw new UnsupportedOperationException( - "Read only is not supported for Grid."); - } - - @Override - public boolean isReadOnly() { - throw new UnsupportedOperationException( - "Read only is not supported for Grid."); - } - }; - } -} diff --git a/server/src/main/java/com/vaadin/ui/components/grid/SingleSelectionModelImpl.java b/server/src/main/java/com/vaadin/ui/components/grid/SingleSelectionModelImpl.java new file mode 100644 index 0000000000..e68487f358 --- /dev/null +++ b/server/src/main/java/com/vaadin/ui/components/grid/SingleSelectionModelImpl.java @@ -0,0 +1,299 @@ +/* + * 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.HashSet; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +import com.vaadin.event.selection.SingleSelectionEvent; +import com.vaadin.event.selection.SingleSelectionListener; +import com.vaadin.shared.Registration; +import com.vaadin.shared.data.DataCommunicatorConstants; +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.SingleSelectionModel; +import com.vaadin.ui.SingleSelect; +import com.vaadin.util.ReflectTools; + +import elemental.json.JsonObject; + +/** + * Single selection model for grid. + * + * @author Vaadin Ltd. + * @since 8.0 + * + * @param + * the type of the selected item in grid. + */ +public class SingleSelectionModelImpl extends AbstractGridExtension + implements SingleSelectionModel { + + private static final Method SELECTION_CHANGE_METHOD = ReflectTools + .findMethod(SingleSelectionListener.class, "accept", + SingleSelectionEvent.class); + + private final Grid grid; + private T selectedItem = null; + + /** + * Constructs a new single selection model for the given grid. + * + * @param grid + * the grid to bind the selection model into + */ + public SingleSelectionModelImpl(Grid grid) { + this.grid = grid; + extend(grid); + registerRpc(new SelectionServerRpc() { + + @Override + public void select(String key) { + setSelectedFromClient(key); + } + + @Override + public void deselect(String key) { + if (isKeySelected(key)) { + setSelectedFromClient(null); + } + } + }); + } + + /** + * 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 addSelectionListener( + SingleSelectionListener listener) { + return addListener(SingleSelectionEvent.class, listener, + SELECTION_CHANGE_METHOD); + } + + @Override + public Optional getSelectedItem() { + return Optional.ofNullable(selectedItem); + } + + @Override + public void deselect(T item) { + Objects.requireNonNull(item, "deselected item cannot be null"); + if (isSelected(item)) { + setSelectedFromServer(null); + } + } + + @Override + public void select(T item) { + Objects.requireNonNull(item, "selected item cannot be null"); + setSelectedFromServer(item); + } + + /** + * Returns whether the given key maps to the currently selected item. + * + * @param key + * the key to test or {@code null} to test whether nothing is + * selected + * @return {@code true} if the key equals the key of the currently selected + * item (or {@code null} if no selection), {@code false} otherwise. + */ + protected boolean isKeySelected(String key) { + return Objects.equals(key, getSelectedKey()); + } + + /** + * Returns the communication key of the selected item or {@code null} if no + * item is selected. + * + * @return the key of the selected item if any, {@code null} otherwise. + */ + protected String getSelectedKey() { + return itemToKey(selectedItem); + } + + /** + * Sets the selected item based on the given communication key. If the key + * is {@code null}, clears the current selection if any. + * + * @param key + * the key of the selected item or {@code null} to clear + * 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); + } + selectedItem = getData(key); + if (selectedItem != null) { + grid.getDataCommunicator().refresh(selectedItem); + } + } + + /** + * Sets the selection based on a client request. Does nothing if the select + * component is {@linkplain Component#isReadOnly()} or if the selection + * would not change. Otherwise updates the selection and fires a selection + * change event with {@code isUserOriginated == true}. + * + * @param key + * the key of the item to select or {@code null} to clear + * selection + */ + protected void setSelectedFromClient(String key) { + if (isKeySelected(key)) { + return; + } + + doSetSelectedKey(key); + fireEvent(new SingleSelectionEvent<>(grid, asSingleSelect(), true)); + } + + /** + * Sets the selection based on server API call. Does nothing if the + * selection would not change; otherwise updates the selection and fires a + * selection change event with {@code isUserOriginated == false}. + * + * @param item + * the item to select or {@code null} to clear selection + */ + protected void setSelectedFromServer(T item) { + // TODO creates a key if item not in data provider + String key = itemToKey(item); + + if (isSelected(item) || isKeySelected(key)) { + return; + } + + doSetSelectedKey(key); + fireEvent(new SingleSelectionEvent<>(grid, asSingleSelect(), false)); + } + + /** + * Returns the communication key assigned to the given item. + * + * @param item + * the item whose key to return + * @return the assigned key + */ + protected String itemToKey(T item) { + if (item == null) { + return null; + } else { + // TODO creates a key if item not in data provider + return grid.getDataCommunicator().getKeyMapper().key(item); + } + } + + @Override + public Set getSelectedItems() { + if (selectedItem != null) { + return new HashSet<>(Arrays.asList(selectedItem)); + } else { + return Collections.emptySet(); + } + } + + @Override + public void generateData(T item, JsonObject jsonObject) { + if (isSelected(item)) { + jsonObject.put(DataCommunicatorConstants.SELECTED, true); + } + } + + @Override + public void remove() { + // when selection model changes, firing an event for selection change + // event fired before removing so that parent is still intact (in case + // needed) + setSelectedFromServer(null); + + super.remove(); + } + + /** + * Gets a wrapper for using this grid as a single select in a binder. + * + * @return a single select wrapper for grid + */ + @Override + public SingleSelect asSingleSelect() { + return new SingleSelect() { + + @Override + public void setValue(T value) { + SingleSelectionModelImpl.this.setSelectedFromServer(value); + } + + @Override + public T getValue() { + return SingleSelectionModelImpl.this.getSelectedItem() + .orElse(null); + } + + @Override + public Registration addValueChangeListener( + com.vaadin.data.HasValue.ValueChangeListener listener) { + return SingleSelectionModelImpl.this + .addSelectionListener(event -> listener.accept(event)); + } + + @Override + public void setRequiredIndicatorVisible( + boolean requiredIndicatorVisible) { + // TODO support required indicator when grid is used in binder ? + throw new UnsupportedOperationException( + "Required indicator is not supported for Grid."); + } + + @Override + public boolean isRequiredIndicatorVisible() { + throw new UnsupportedOperationException( + "Required indicator is not supported for Grid."); + } + + @Override + public void setReadOnly(boolean readOnly) { + // TODO support read only when grid is used in binder ? + throw new UnsupportedOperationException( + "Read only is not supported for Grid."); + } + + @Override + public boolean isReadOnly() { + throw new UnsupportedOperationException( + "Read only is not supported for Grid."); + } + }; + } +} 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, BeanWithEnums> { + public class TestEnumSetToStringConverter + implements Converter, String> { + @Override + public Result convertToModel(Set value, + ValueContext context) { + return Result.ok(value.stream().map(TestEnum::name) + .collect(Collectors.joining(","))); + } + + @Override + public Set 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 { + + public CustomMultiSelectModel(Grid grid) { + super(grid); + } + + @Override + public void updateSelection(Set addedItems, Set removedItems, + boolean userOriginated) { + super.updateSelection(addedItems, removedItems, userOriginated); + } + + } + + private Binder> converterBinder = new Binder<>(); + private Grid grid; + private MultiSelect 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 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 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 grid = new Grid<>(); + CustomMultiSelectModel model = new CustomMultiSelectModel(grid); + grid.setSelectionModel(model); + grid.setItems(Sex.values()); + MultiSelect select = grid.asMultiSelect(); + + List selected = new ArrayList<>(); + List 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, Person> { @@ -29,7 +30,7 @@ public class GridAsSingleSelectInBinder } } - private class CustomSingleSelectModel extends SingleSelectionModel { + private class CustomSingleSelectModel extends SingleSelectionModelImpl { public CustomSingleSelectModel(Grid 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 grid; + private MultiSelectionModelImpl selectionModel; + private Capture> currentSelectionCapture; + private Capture> oldSelectionCapture; + private AtomicInteger events; + + public static class CustomMultiSelectionModel + extends MultiSelectionModelImpl { + public final Map generatedData = new LinkedHashMap<>(); + + public CustomMultiSelectionModel(Grid 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 customGrid = new Grid<>(); + customGrid.setSelectionModel(new MultiSelectionModelImpl<>(customGrid)); + customGrid.setItems("Foo", "Bar", "Baz"); + + List selectionChanges = new ArrayList<>(); + Capture> oldSelectionCapture = new Capture<>(); + ((MultiSelectionModelImpl) 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 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 gridWithStrings = new Grid<>(); + gridWithStrings + .setSelectionModel(new MultiSelectionModelImpl<>(gridWithStrings)); + gridWithStrings.setItems("Foo", "Bar", "Baz"); + + GridSelectionModel 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 Set 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> selectionListener = new AtomicReference<>(); + Registration registration = Mockito.mock(Registration.class); + Grid grid = new Grid<>(); + grid.setItems("foo", "bar"); + String value = "foo"; + SingleSelectionModelImpl select = new SingleSelectionModelImpl( + grid) { + @Override + public Registration addSelectionListener( + SingleSelectionListener listener) { + selectionListener.set(listener); + return registration; + } + + @Override + public Optional getSelectedItem() { + return Optional.of(value); + } + }; + + AtomicReference> 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,61 +22,123 @@ 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 { - public void switchSelectionModel() { - // just switch selection model to cause event - setSelectionModel(new SingleSelectionModel(this)); + public static class CustomSingleSelectionModel + extends SingleSelectionModelImpl { + public final Map generatedData = new LinkedHashMap<>(); + + public CustomSingleSelectionModel(Grid 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 selectionChanges; private Grid grid; - private SingleSelectionModel selectionModel; + private SingleSelectionModelImpl selectionModel; @Before public void setUp() { grid = new Grid<>(); grid.setItems(PERSON_A, PERSON_B, PERSON_C); - selectionModel = (SingleSelectionModel) grid + selectionModel = (SingleSelectionModelImpl) 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 customGrid = new Grid<>(); customGrid.setItems("Foo", "Bar", "Baz"); List selectionChanges = new ArrayList<>(); - ((SingleSelectionModel) customGrid.getSelectionModel()) - .addSelectionChangeListener( - e -> selectionChanges.add(e.getValue())); + ((SingleSelectionModelImpl) 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 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 gridWithStrings = new Grid<>(); @@ -210,10 +274,10 @@ public class GridSingleSelectionModelTest { Grid grid = new Grid<>(); grid.setItems("foo", "bar"); String value = "foo"; - SingleSelectionModel select = new SingleSelectionModel( + SingleSelectionModelImpl select = new SingleSelectionModelImpl( grid) { @Override - public Registration addSelectionChangeListener( + public Registration addSelectionListener( SingleSelectionListener listener) { selectionListener.set(listener); return registration; @@ -226,11 +290,10 @@ public class GridSingleSelectionModelTest { }; AtomicReference> 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, -- cgit v1.2.3