]> source.dussan.org Git - vaadin-framework.git/commitdiff
Server side Grid can open details on the client side (#16644)
authorHenrik Paul <henrik@vaadin.com>
Mon, 9 Mar 2015 12:31:37 +0000 (14:31 +0200)
committerHenrik Paul <henrik@vaadin.com>
Tue, 17 Mar 2015 10:59:05 +0000 (12:59 +0200)
Change-Id: Ibff5a83b3a09c7c530926dadae9138ba3823f27a

client/src/com/vaadin/client/connectors/GridConnector.java
client/src/com/vaadin/client/connectors/RpcDataSourceConnector.java
client/src/com/vaadin/client/widget/grid/DetailsGenerator.java
client/src/com/vaadin/client/widgets/Grid.java
server/src/com/vaadin/data/RpcDataProviderExtension.java
server/src/com/vaadin/ui/Grid.java
shared/src/com/vaadin/shared/ui/grid/GridState.java
uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeatures.java
uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridDetailsServerTest.java [new file with mode: 0644]

index 55f07ecf85f56e08a04ff4e2714a1385e4cff885..f476982c158bac57aa9c5ece2dcb94b3719d1689 100644 (file)
@@ -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<JsonObject> {
@@ -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);
+        }
+    }
 }
index f8d6ebcb620426160b620d8b29030e63542605fb..ae4249de781b1e1e8ae9f141310e4df34309d910 100644 (file)
@@ -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<JsonObject> {
 
         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<JsonObject> 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);
     }
 }
index 264aa4e614cad82d17f777d581727cf4c37a12ce..309e3f1ea3d2bf63ff005806f4b669a1aa3d14fc 100644 (file)
@@ -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) {
index b3906591c0bf90eb13ab0912bede42ad6cb6ae13..f4aaf798b7b6c336e23fd9afd21931da7957fcaa 100644 (file)
@@ -6326,10 +6326,17 @@ public class Grid<T> extends ResizeComposite implements
      * @since
      * @param detailsGenerator
      *            the details generator to set
+     * @throws IllegalArgumentException
+     *             if detailsGenerator is <code>null</code>;
      */
     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
index 991cb0537d4d96696daade3e5f31f72c6ef44c23..cf2284a62e831bba6460cb540c45b93731a1cc09 100644 (file)
@@ -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.
          * <p>
@@ -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<Object> visibleDetails = new HashSet<Object>();
+
     /**
      * 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.
+     * <p>
+     * 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
+     *            <code>true</code> to show the details, <code>false</code> 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 <code>true</code> iff the detials are visible for the item. This
+     *         might return <code>true</code> even if the row is not currently
+     *         visible in the DOM
+     */
+    public boolean isDetailsVisible(Object itemId) {
+        return visibleDetails.contains(itemId);
+    }
 }
index 22ef0333c211ef58db009d5fd537b04c91f268ac..b56bb0d036269f563e14c8f1217c365151fe51bb 100644 (file)
@@ -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 <code>null</code> 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.
+     * <p>
+     * The currently opened row details will be re-rendered.
+     * 
+     * @since
+     * @param detailsGenerator
+     *            the details generator to set
+     * @throws IllegalArgumentException
+     *             if detailsGenerator is <code>null</code>;
+     */
+    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
+     *            <code>true</code> to show the details, or <code>false</code>
+     *            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 <code>true</code> iff the details are visible
+     */
+    public boolean isDetailsVisible(Object itemId) {
+        return datasourceExtension.isDetailsVisible(itemId);
+    }
 }
index 7018df1413adae97589aa37e27413285bf47ca6c..81e1827420f9682ba90be3b17d6beec84bbf2243 100644 (file)
@@ -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.
      */
index e5a46894b88e0ff128d79bf5033ea968bf9a6679..f0c4b3d9c06173d569a401be1fa3f1dae8af8371 100644 (file)
@@ -248,6 +248,8 @@ public class GridBasicFeatures extends AbstractComponentTest<Grid> {
 
         addFilterActions();
 
+        createDetailsActions();
+
         this.grid = grid;
         return grid;
     }
@@ -1051,6 +1053,18 @@ public class GridBasicFeatures extends AbstractComponentTest<Grid> {
                 }, null);
     }
 
+    private void createDetailsActions() {
+        createBooleanAction("firstItemId", "Details", false,
+                new Command<Grid, Boolean>() {
+                    @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 (file)
index 0000000..01d2ba5
--- /dev/null
@@ -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);
+    }
+}