From 84c143dd76ed1d27d03c0d695e7218b477d008fe Mon Sep 17 00:00:00 2001 From: Henrik Paul Date: Mon, 9 Mar 2015 14:31:37 +0200 Subject: [PATCH] Server side Grid can open details on the client side (#16644) Change-Id: Ibff5a83b3a09c7c530926dadae9138ba3823f27a --- .../client/connectors/GridConnector.java | 26 +++++- .../connectors/RpcDataSourceConnector.java | 53 ++++++++++- .../client/widget/grid/DetailsGenerator.java | 1 + .../src/com/vaadin/client/widgets/Grid.java | 7 ++ .../vaadin/data/RpcDataProviderExtension.java | 72 ++++++++++++++- server/src/com/vaadin/ui/Grid.java | 91 +++++++++++++++++++ .../com/vaadin/shared/ui/grid/GridState.java | 10 ++ .../grid/basicfeatures/GridBasicFeatures.java | 14 +++ .../server/GridDetailsServerTest.java | 76 ++++++++++++++++ 9 files changed, 341 insertions(+), 9 deletions(-) create mode 100644 uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridDetailsServerTest.java diff --git a/client/src/com/vaadin/client/connectors/GridConnector.java b/client/src/com/vaadin/client/connectors/GridConnector.java index 55f07ecf85..f476982c15 100644 --- a/client/src/com/vaadin/client/connectors/GridConnector.java +++ b/client/src/com/vaadin/client/connectors/GridConnector.java @@ -31,6 +31,7 @@ import java.util.logging.Logger; import com.google.gwt.core.client.Scheduler; import com.google.gwt.core.client.Scheduler.ScheduledCommand; import com.google.gwt.dom.client.NativeEvent; +import com.google.gwt.user.client.ui.Label; import com.google.gwt.user.client.ui.Widget; import com.vaadin.client.ComponentConnector; import com.vaadin.client.ConnectorHierarchyChangeEvent; @@ -45,6 +46,7 @@ import com.vaadin.client.ui.AbstractHasComponentsConnector; import com.vaadin.client.ui.SimpleManagedLayout; import com.vaadin.client.widget.grid.CellReference; import com.vaadin.client.widget.grid.CellStyleGenerator; +import com.vaadin.client.widget.grid.DetailsGenerator; import com.vaadin.client.widget.grid.EditorHandler; import com.vaadin.client.widget.grid.RowReference; import com.vaadin.client.widget.grid.RowStyleGenerator; @@ -101,7 +103,7 @@ import elemental.json.JsonValue; */ @Connect(com.vaadin.ui.Grid.class) public class GridConnector extends AbstractHasComponentsConnector implements - SimpleManagedLayout { + SimpleManagedLayout, RpcDataSourceConnector.DetailsListener { private static final class CustomCellStyleGenerator implements CellStyleGenerator { @@ -360,6 +362,14 @@ public class GridConnector extends AbstractHasComponentsConnector implements } } + private class CustomDetailsGenerator implements DetailsGenerator { + @Override + public Widget getDetails(int rowIndex) { + // TODO + return new Label("[todo]"); + } + } + /** * Maps a generated column id to a grid column instance */ @@ -501,7 +511,11 @@ public class GridConnector extends AbstractHasComponentsConnector implements }); getWidget().setEditorHandler(new CustomEditorHandler()); + + getWidget().setDetailsGenerator(new CustomDetailsGenerator()); + getLayoutManager().registerDependency(this, getWidget().getElement()); + layout(); } @@ -994,4 +1008,14 @@ public class GridConnector extends AbstractHasComponentsConnector implements public void layout() { getWidget().onResize(); } + + @Override + public void reapplyDetailsVisibility(int rowIndex, JsonObject row) { + if (row.hasKey(GridState.JSONKEY_DETAILS_VISIBLE) + && row.getBoolean(GridState.JSONKEY_DETAILS_VISIBLE)) { + getWidget().setDetailsVisible(rowIndex, true); + } else { + getWidget().setDetailsVisible(rowIndex, false); + } + } } diff --git a/client/src/com/vaadin/client/connectors/RpcDataSourceConnector.java b/client/src/com/vaadin/client/connectors/RpcDataSourceConnector.java index f8d6ebcb62..ae4249de78 100644 --- a/client/src/com/vaadin/client/connectors/RpcDataSourceConnector.java +++ b/client/src/com/vaadin/client/connectors/RpcDataSourceConnector.java @@ -17,6 +17,7 @@ package com.vaadin.client.connectors; import java.util.ArrayList; +import java.util.List; import com.vaadin.client.ServerConnector; import com.vaadin.client.data.AbstractRemoteDataSource; @@ -43,6 +44,28 @@ import elemental.json.JsonObject; @Connect(com.vaadin.data.RpcDataProviderExtension.class) public class RpcDataSourceConnector extends AbstractExtensionConnector { + /** + * A callback interface to let {@link GridConnector} know that detail + * visibilities might have changed. + * + * @since + * @author Vaadin Ltd + */ + interface DetailsListener { + + /** + * A request to verify (and correct) the visibility for a row, given + * updated metadata. + * + * @param rowIndex + * the index of the row that should be checked + * @param row + * the row object to check visibility for + * @see GridState#JSONKEY_DETAILS_VISIBLE + */ + void reapplyDetailsVisibility(int rowIndex, JsonObject row); + } + public class RpcDataSource extends AbstractRemoteDataSource { protected RpcDataSource() { @@ -56,27 +79,28 @@ public class RpcDataSourceConnector extends AbstractExtensionConnector { rows.add(rowObject); } - dataSource.setRowData(firstRow, rows); + RpcDataSource.this.setRowData(firstRow, rows); } @Override public void removeRowData(int firstRow, int count) { - dataSource.removeRowData(firstRow, count); + RpcDataSource.this.removeRowData(firstRow, count); } @Override public void insertRowData(int firstRow, int count) { - dataSource.insertRowData(firstRow, count); + RpcDataSource.this.insertRowData(firstRow, count); } @Override public void resetDataAndSize(int size) { - dataSource.resetDataAndSize(size); + RpcDataSource.this.resetDataAndSize(size); } }); } private DataRequestRpc rpcProxy = getRpcProxy(DataRequestRpc.class); + private DetailsListener detailsListener; @Override protected void requestRows(int firstRowIndex, int numberOfRows, @@ -170,7 +194,24 @@ public class RpcDataSourceConnector extends AbstractExtensionConnector { if (!handle.isPinned()) { rpcProxy.setPinned(key, false); } + } + + void setDetailsListener(DetailsListener detailsListener) { + this.detailsListener = detailsListener; + } + @Override + protected void setRowData(int firstRowIndex, List rowData) { + super.setRowData(firstRowIndex, rowData); + + /* + * Intercepting details information from the data source, rerouting + * them back to the GridConnector (as a details listener) + */ + for (int i = 0; i < rowData.size(); i++) { + detailsListener.reapplyDetailsVisibility(firstRowIndex + i, + rowData.get(i)); + } } } @@ -178,6 +219,8 @@ public class RpcDataSourceConnector extends AbstractExtensionConnector { @Override protected void extend(ServerConnector target) { - ((GridConnector) target).setDataSource(dataSource); + GridConnector gridConnector = (GridConnector) target; + dataSource.setDetailsListener(gridConnector); + gridConnector.setDataSource(dataSource); } } diff --git a/client/src/com/vaadin/client/widget/grid/DetailsGenerator.java b/client/src/com/vaadin/client/widget/grid/DetailsGenerator.java index 264aa4e614..309e3f1ea3 100644 --- a/client/src/com/vaadin/client/widget/grid/DetailsGenerator.java +++ b/client/src/com/vaadin/client/widget/grid/DetailsGenerator.java @@ -25,6 +25,7 @@ import com.google.gwt.user.client.ui.Widget; */ public interface DetailsGenerator { + /** A details generator that provides no details */ public static final DetailsGenerator NULL = new DetailsGenerator() { @Override public Widget getDetails(int rowIndex) { diff --git a/client/src/com/vaadin/client/widgets/Grid.java b/client/src/com/vaadin/client/widgets/Grid.java index b3906591c0..f4aaf798b7 100644 --- a/client/src/com/vaadin/client/widgets/Grid.java +++ b/client/src/com/vaadin/client/widgets/Grid.java @@ -6326,10 +6326,17 @@ public class Grid extends ResizeComposite implements * @since * @param detailsGenerator * the details generator to set + * @throws IllegalArgumentException + * if detailsGenerator is null; */ public void setDetailsGenerator(DetailsGenerator detailsGenerator) throws IllegalArgumentException { + if (detailsGenerator == null) { + throw new IllegalArgumentException( + "Details generator may not be null"); + } + this.detailsGenerator = detailsGenerator; // this will refresh all visible spacers diff --git a/server/src/com/vaadin/data/RpcDataProviderExtension.java b/server/src/com/vaadin/data/RpcDataProviderExtension.java index 991cb0537d..cf2284a62e 100644 --- a/server/src/com/vaadin/data/RpcDataProviderExtension.java +++ b/server/src/com/vaadin/data/RpcDataProviderExtension.java @@ -95,7 +95,7 @@ public class RpcDataProviderExtension extends AbstractExtension { // private implementation } - void setActiveRange(Range newActiveRange) { + public void setActiveRange(Range newActiveRange) { final Range[] removed = activeRange.partitionWith(newActiveRange); final Range[] added = newActiveRange.partitionWith(activeRange); @@ -163,7 +163,7 @@ public class RpcDataProviderExtension extends AbstractExtension { return String.valueOf(rollingIndex++); } - String getKey(Object itemId) { + public String getKey(Object itemId) { String key = itemIdToKey.get(itemId); if (key == null) { key = nextKey(); @@ -240,6 +240,20 @@ public class RpcDataProviderExtension extends AbstractExtension { return itemIds; } + /** + * Gets the row index for a given item. + * + * @since + * @param itemId + * the item id of the item for which to get the item + * @return the index of the item, or -1 if no such item could be found + */ + @SuppressWarnings("boxing") + public int getIndex(Object itemId) { + Integer integer = indexToItemId.inverse().get(itemId); + return integer != null ? integer : -1; + } + /** * Pin an item id to be cached indefinitely. *

@@ -304,7 +318,7 @@ public class RpcDataProviderExtension extends AbstractExtension { return pinnedItemIds.contains(itemId); } - Object itemIdAtIndex(int index) { + private Object itemIdAtIndex(int index) { return indexToItemId.get(Integer.valueOf(index)); } } @@ -727,6 +741,12 @@ public class RpcDataProviderExtension extends AbstractExtension { /** Size possibly changed with a bare ItemSetChangeEvent */ private boolean bareItemSetTriggeredSizeChange = false; + /** + * This map represents all the details that are user-defined as visible. + * This does not reflect the status in the DOM. + */ + private Set visibleDetails = new HashSet(); + /** * Creates a new data provider using the given container. * @@ -859,6 +879,10 @@ public class RpcDataProviderExtension extends AbstractExtension { rowObject.put(GridState.JSONKEY_DATA, rowData); rowObject.put(GridState.JSONKEY_ROWKEY, keyMapper.getKey(itemId)); + if (visibleDetails.contains(itemId)) { + rowObject.put(GridState.JSONKEY_DETAILS_VISIBLE, true); + } + rowReference.set(itemId); CellStyleGenerator cellStyleGenerator = grid.getCellStyleGenerator(); @@ -1116,4 +1140,46 @@ public class RpcDataProviderExtension extends AbstractExtension { return Logger.getLogger(RpcDataProviderExtension.class.getName()); } + /** + * Marks a row's details to be visible or hidden. + *

+ * If that row is currently in the client side's cache, this information + * will be sent over to the client. + * + * @since + * @param itemId + * the id of the item of which to change the details visibility + * @param visible + * true to show the details, false to + * hide + */ + public void setDetailsVisible(Object itemId, boolean visible) { + final boolean modified; + if (visible) { + modified = visibleDetails.add(itemId); + } else { + modified = visibleDetails.remove(itemId); + } + + int rowIndex = keyMapper.getIndex(itemId); + boolean modifiedRowIsActive = activeRowHandler.activeRange + .contains(rowIndex); + if (modified && modifiedRowIsActive) { + updateRowData(itemId); + } + } + + /** + * Checks whether the details for a row is marked as visible. + * + * @since + * @param itemId + * the id of the item of which to check the visibility + * @return true iff the detials are visible for the item. This + * might return true even if the row is not currently + * visible in the DOM + */ + public boolean isDetailsVisible(Object itemId) { + return visibleDetails.contains(itemId); + } } diff --git a/server/src/com/vaadin/ui/Grid.java b/server/src/com/vaadin/ui/Grid.java index 22ef0333c2..b56bb0d036 100644 --- a/server/src/com/vaadin/ui/Grid.java +++ b/server/src/com/vaadin/ui/Grid.java @@ -164,6 +164,34 @@ import elemental.json.JsonValue; public class Grid extends AbstractComponent implements SelectionNotifier, SortNotifier, SelectiveRenderer, ItemClickNotifier { + /** + * A callback interface for generating details for a particular row in Grid. + * + * @since + * @author Vaadin Ltd + */ + public interface DetailsGenerator extends Serializable { + + /** A details generator that provides no details */ + public DetailsGenerator NULL = new DetailsGenerator() { + @Override + public Component getDetails(RowReference rowReference) { + return null; + } + }; + + /** + * This method is called for whenever a new details row needs to be + * generated. + * + * @param rowReference + * the reference for the row for which to generate details + * @return the details for the given row, or null to leave + * the details empty. + */ + Component getDetails(RowReference rowReference); + } + /** * Custom field group that allows finding property types before an item has * been bound. @@ -2888,6 +2916,8 @@ public class Grid extends AbstractComponent implements SelectionNotifier, private EditorErrorHandler editorErrorHandler = new DefaultEditorErrorHandler(); + private DetailsGenerator detailsGenerator = DetailsGenerator.NULL; + private static final Method SELECTION_CHANGE_METHOD = ReflectTools .findMethod(SelectionListener.class, "select", SelectionEvent.class); @@ -5093,4 +5123,65 @@ public class Grid extends AbstractComponent implements SelectionNotifier, public void recalculateColumnWidths() { getRpcProxy(GridClientRpc.class).recalculateColumnWidths(); } + + /** + * Sets a new details generator for row details. + *

+ * The currently opened row details will be re-rendered. + * + * @since + * @param detailsGenerator + * the details generator to set + * @throws IllegalArgumentException + * if detailsGenerator is null; + */ + public void setDetailsGenerator(DetailsGenerator detailsGenerator) + throws IllegalArgumentException { + if (detailsGenerator == null) { + throw new IllegalArgumentException( + "Details generator may not be null"); + } else if (detailsGenerator == this.detailsGenerator) { + return; + } + + this.detailsGenerator = detailsGenerator; + + getLogger().warning("[[details]] update details on generator swap"); + } + + /** + * Gets the current details generator for row details. + * + * @since + * @return the detailsGenerator the current details generator + */ + public DetailsGenerator getDetailsGenerator() { + return detailsGenerator; + } + + /** + * Shows or hides the details for a specific item. + * + * @since + * @param itemId + * the id of the item for which to set details visibility + * @param visible + * true to show the details, or false + * to hide them + */ + public void setDetailsVisible(Object itemId, boolean visible) { + datasourceExtension.setDetailsVisible(itemId, visible); + } + + /** + * Checks whether details are visible for the given item. + * + * @since + * @param itemId + * the id of the item for which to check details visibility + * @return true iff the details are visible + */ + public boolean isDetailsVisible(Object itemId) { + return datasourceExtension.isDetailsVisible(itemId); + } } diff --git a/shared/src/com/vaadin/shared/ui/grid/GridState.java b/shared/src/com/vaadin/shared/ui/grid/GridState.java index 7018df1413..81e1827420 100644 --- a/shared/src/com/vaadin/shared/ui/grid/GridState.java +++ b/shared/src/com/vaadin/shared/ui/grid/GridState.java @@ -102,6 +102,16 @@ public class GridState extends AbstractComponentState { */ public static final String JSONKEY_CELLSTYLES = "cs"; + /** + * The key that tells whether details are visible for the row + * + * @see com.vaadin.ui.Grid#setDetailsGenerator(com.vaadin.ui.Grid.DetailsGenerator) + * @see com.vaadin.ui.Grid#setDetailsVisible(Object, boolean) + * @see com.vaadin.shared.data.DataProviderRpc#setRowData(int, + * elemental.json.JsonArray) + * */ + public static final String JSONKEY_DETAILS_VISIBLE = "dv"; + /** * Columns in grid. */ diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeatures.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeatures.java index e5a46894b8..f0c4b3d9c0 100644 --- a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeatures.java +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeatures.java @@ -248,6 +248,8 @@ public class GridBasicFeatures extends AbstractComponentTest { addFilterActions(); + createDetailsActions(); + this.grid = grid; return grid; } @@ -1051,6 +1053,18 @@ public class GridBasicFeatures extends AbstractComponentTest { }, null); } + private void createDetailsActions() { + createBooleanAction("firstItemId", "Details", false, + new Command() { + @Override + @SuppressWarnings("boxing") + public void execute(Grid g, Boolean visible, Object data) { + g.setDetailsVisible(g.getContainerDataSource() + .firstItemId(), visible); + } + }); + } + @Override protected Integer getTicketNumber() { return 12829; diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridDetailsServerTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridDetailsServerTest.java new file mode 100644 index 0000000000..01d2ba55eb --- /dev/null +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridDetailsServerTest.java @@ -0,0 +1,76 @@ +/* + * Copyright 2000-2014 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.tests.components.grid.basicfeatures.server; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; + +import org.junit.Before; +import org.junit.Test; +import org.openqa.selenium.NoSuchElementException; + +import com.vaadin.testbench.annotations.RunLocally; +import com.vaadin.testbench.parallel.Browser; +import com.vaadin.tests.components.grid.basicfeatures.GridBasicFeaturesTest; + +@RunLocally(Browser.PHANTOMJS) +public class GridDetailsServerTest extends GridBasicFeaturesTest { + private static final String[] FIRST_ITEM_DETAILS = new String[] { + "Component", "Details", "firstItemId" }; + + @Before + public void setUp() { + openTestURL(); + } + + @Test + public void openVisibleDetails() { + try { + getGridElement().getDetails(0); + fail("Expected NoSuchElementException"); + } catch (NoSuchElementException ignore) { + // expected + } + selectMenuPath(FIRST_ITEM_DETAILS); + assertNotNull("details should've opened", getGridElement() + .getDetails(0)); + } + + @Test(expected = NoSuchElementException.class) + public void closeVisibleDetails() { + selectMenuPath(FIRST_ITEM_DETAILS); + selectMenuPath(FIRST_ITEM_DETAILS); + getGridElement().getDetails(0); + } + + @Test + public void openDetailsOutsideOfActiveRange() { + getGridElement().scroll(10000); + selectMenuPath(FIRST_ITEM_DETAILS); + getGridElement().scroll(0); + assertNotNull("details should've been opened", getGridElement() + .getDetails(0)); + } + + @Test(expected = NoSuchElementException.class) + public void closeDetailsOutsideOfActiveRange() { + selectMenuPath(FIRST_ITEM_DETAILS); + getGridElement().scroll(10000); + selectMenuPath(FIRST_ITEM_DETAILS); + getGridElement().scroll(0); + getGridElement().getDetails(0); + } +} -- 2.39.5