]> source.dussan.org Git - vaadin-framework.git/commitdiff
Initial support for multiple headers in new Grid
authorJohannes Dahlström <johannesd@vaadin.com>
Wed, 21 Sep 2016 10:08:16 +0000 (13:08 +0300)
committerVaadin Code Review <review@vaadin.com>
Tue, 27 Sep 2016 14:14:38 +0000 (14:14 +0000)
Change-Id: I7a3fa34749322451ab5cef4465d4d7c76029c097

client/src/main/java/com/vaadin/client/connectors/grid/GridConnector.java
server/src/main/java/com/vaadin/ui/Grid.java
server/src/main/java/com/vaadin/ui/components/grid/Header.java [new file with mode: 0644]
server/src/main/java/com/vaadin/ui/components/grid/StaticSection.java [new file with mode: 0644]
server/src/test/java/com/vaadin/tests/server/component/grid/GridHeaderFooterTest.java [new file with mode: 0644]
shared/src/main/java/com/vaadin/shared/ui/grid/GridState.java
shared/src/main/java/com/vaadin/shared/ui/grid/SectionState.java [new file with mode: 0644]
uitest/src/main/java/com/vaadin/tests/components/grid/GridColumnResizing.java
uitest/src/main/java/com/vaadin/tests/components/grid/basics/GridBasics.java
uitest/src/test/java/com/vaadin/tests/components/grid/basics/GridHeaderFooterTest.java [new file with mode: 0644]

index ed07d1d3f1526af4073429771b1dec6b42d0d945..0da4ec3bcced42dc6486ea4394e38efab4ef5bb6 100644 (file)
@@ -34,6 +34,7 @@ import com.vaadin.client.HasComponentsConnector;
 import com.vaadin.client.MouseEventDetailsBuilder;
 import com.vaadin.client.TooltipInfo;
 import com.vaadin.client.WidgetUtil;
+import com.vaadin.client.annotations.OnStateChange;
 import com.vaadin.client.connectors.AbstractListingConnector;
 import com.vaadin.client.data.DataSource;
 import com.vaadin.client.ui.SimpleManagedLayout;
@@ -49,6 +50,7 @@ import com.vaadin.client.widget.grid.sort.SortEvent;
 import com.vaadin.client.widget.grid.sort.SortOrder;
 import com.vaadin.client.widgets.Grid;
 import com.vaadin.client.widgets.Grid.Column;
+import com.vaadin.client.widgets.Grid.HeaderRow;
 import com.vaadin.shared.MouseEventDetails;
 import com.vaadin.shared.data.DataCommunicatorConstants;
 import com.vaadin.shared.data.selection.SelectionModel;
@@ -59,6 +61,8 @@ import com.vaadin.shared.ui.grid.GridConstants;
 import com.vaadin.shared.ui.grid.GridConstants.Section;
 import com.vaadin.shared.ui.grid.GridServerRpc;
 import com.vaadin.shared.ui.grid.GridState;
+import com.vaadin.shared.ui.grid.SectionState;
+import com.vaadin.shared.ui.grid.SectionState.RowState;
 
 import elemental.json.JsonObject;
 
@@ -102,6 +106,8 @@ public class GridConnector
 
     /* Map to keep track of all added columns */
     private Map<Column<?, JsonObject>, String> columnToIdMap = new HashMap<>();
+    private Map<String, Column<?, JsonObject>> idToColumn = new HashMap<>();
+
     /* Child component list for HasComponentsConnector */
     private List<ComponentConnector> childComponents;
     private SpaceSelectHandler<JsonObject> spaceSelectHandler;
@@ -109,16 +115,27 @@ public class GridConnector
     private ItemClickHandler itemClickHandler = new ItemClickHandler();
 
     /**
-     * Gets the string identifier of a {@link Column} in this grid.
+     * Gets the string identifier of the given column in this grid.
      *
      * @param column
-     *            the column for which the identifier is to be retrieved for
-     * @return the string identifying the given column in this grid
+     *            the column whose id to get
+     * @return the string id of the column
      */
-    public String getColumnId(Grid.Column<?, ?> column) {
+    public String getColumnId(Column<?, ?> column) {
         return columnToIdMap.get(column);
     }
 
+    /**
+     * Gets the column corresponding to the given string identifier.
+     *
+     * @param columnId
+     *            the id of the column to get
+     * @return the column with the given id
+     */
+    public Column<?, ?> getColumn(String columnId) {
+        return idToColumn.get(columnId);
+    }
+
     @Override
     @SuppressWarnings("unchecked")
     public Grid<JsonObject> getWidget() {
@@ -202,6 +219,28 @@ public class GridConnector
         layout();
     }
 
+    @OnStateChange("header")
+    void updateHeader() {
+        final SectionState state = getState().header;
+        final Grid<JsonObject> grid = getWidget();
+
+        while (grid.getHeaderRowCount() > 0) {
+            grid.removeHeaderRow(0);
+        }
+
+        for (RowState rowState : state.rows) {
+            HeaderRow row = grid.appendHeaderRow();
+            rowState.cells.forEach((columnId, cellState) -> {
+                row.getCell(getColumn(columnId)).setText(cellState.text);
+            });
+        }
+
+        if (grid.getHeaderRowCount() > 0) {
+            // TODO Default header handling to be added in a later patch
+            grid.setDefaultHeaderRow(grid.getHeaderRow(0));
+        }
+    }
+
     @Override
     public void setDataSource(DataSource<JsonObject> dataSource) {
         super.setDataSource(dataSource);
@@ -228,6 +267,7 @@ public class GridConnector
                 .containsValue(id) : "Column with given id already exists.";
         getWidget().addColumn(column);
         columnToIdMap.put(column, id);
+        idToColumn.put(id, column);
     }
 
     /**
@@ -241,7 +281,8 @@ public class GridConnector
         assert columnToIdMap
                 .containsKey(column) : "Given Column does not exist.";
         getWidget().removeColumn(column);
-        columnToIdMap.remove(column);
+        String id = columnToIdMap.remove(column);
+        idToColumn.remove(id);
     }
 
     @Override
index 94f8c9aee798b3dbe521d6e8cf813ee414695aa4..3777decbe983228da3475caa5677e4a7f86e3dcf 100644 (file)
@@ -49,6 +49,8 @@ import com.vaadin.shared.ui.grid.GridConstants.Section;
 import com.vaadin.shared.ui.grid.GridServerRpc;
 import com.vaadin.shared.ui.grid.GridState;
 import com.vaadin.shared.ui.grid.HeightMode;
+import com.vaadin.shared.ui.grid.SectionState;
+import com.vaadin.ui.components.grid.Header;
 import com.vaadin.ui.renderers.AbstractRenderer;
 import com.vaadin.ui.renderers.Renderer;
 import com.vaadin.ui.renderers.TextRenderer;
@@ -512,7 +514,7 @@ public class Grid<T> extends AbstractSingleSelect<T> implements HasComponents {
             if (rowKey != null) {
                 item = getDataCommunicator().getKeyMapper().get(rowKey);
             }
-            fireEvent(new GridContextClickEvent<T>(Grid.this, details, section,
+            fireEvent(new GridContextClickEvent<>(Grid.this, details, section,
                     rowIndex, item, getColumn(columnId)));
         }
 
@@ -824,6 +826,15 @@ public class Grid<T> extends AbstractSingleSelect<T> implements HasComponents {
             super.extend(grid);
         }
 
+        /**
+         * Returns the identifier used with this Column in communication.
+         *
+         * @return the identifier string
+         */
+        public String getId() {
+            return getState(false).id;
+        }
+
         /**
          * Sets the identifier to use with this Column in communication.
          *
@@ -1408,6 +1419,53 @@ public class Grid<T> extends AbstractSingleSelect<T> implements HasComponents {
         }
     }
 
+    /**
+     * A header row in a Grid.
+     */
+    public interface HeaderRow extends Serializable {
+
+        /**
+         * Returns the cell on this row corresponding to the given column id.
+         * 
+         * @param columnId
+         *            the id of the column whose header cell to get
+         * @return the header cell
+         */
+        public HeaderCell getCell(String columnId);
+
+        /**
+         * Returns the cell on this row corresponding to the given column.
+         * 
+         * @param column
+         *            the column whose header cell to get
+         * @return the header cell
+         */
+        public default HeaderCell getCell(Column<?, ?> column) {
+            return getCell(column.getId());
+        }
+    }
+
+    /**
+     * An individual cell on a Grid header row.
+     */
+    public interface HeaderCell extends Serializable {
+
+        /**
+         * Returns the textual caption of this cell.
+         * 
+         * @return the header caption
+         */
+        public String getText();
+
+        /**
+         * Sets the textual caption of this cell.
+         * 
+         * @param text
+         *            the header caption to set
+         */
+        public void setText(String text);
+    }
+
     private KeyMapper<Column<T, ?>> columnKeys = new KeyMapper<>();
     private Set<Column<T, ?>> columnSet = new LinkedHashSet<>();
     private List<SortOrder<Column<T, ?>>> sortOrder = new ArrayList<>();
@@ -1416,15 +1474,26 @@ public class Grid<T> extends AbstractSingleSelect<T> implements HasComponents {
     private StyleGenerator<T> styleGenerator = item -> null;
     private DescriptionGenerator<T> descriptionGenerator;
 
+    private Header header = new Header() {
+        @Override
+        protected SectionState getState(boolean markAsDirty) {
+            return Grid.this.getState(markAsDirty).header;
+        }
+    };
+
     /**
      * Constructor for the {@link Grid} component.
      */
     public Grid() {
         setSelectionModel(new SingleSelection());
         registerRpc(new GridServerRpcImpl());
+
+        appendHeaderRow();
+
         detailsManager = new DetailsManager<>();
         addExtension(detailsManager);
         addDataGenerator(detailsManager);
+
         addDataGenerator((item, json) -> {
             String styleName = styleGenerator.apply(item);
             if (styleName != null && !styleName.isEmpty()) {
@@ -1455,8 +1524,6 @@ public class Grid<T> extends AbstractSingleSelect<T> implements HasComponents {
      *            the value provider
      * @param renderer
      *            the column value class
-     * @param <T>
-     *            the type of this grid
      * @param <V>
      *            the column value type
      *
@@ -1467,13 +1534,23 @@ public class Grid<T> extends AbstractSingleSelect<T> implements HasComponents {
     public <V> Column<T, V> addColumn(String caption,
             Function<T, ? extends V> valueProvider,
             AbstractRenderer<? super T, V> renderer) {
-        Column<T, V> column = new Column<>(caption, valueProvider, renderer);
+        final Column<T, V> column = new Column<>(caption, valueProvider,
+                renderer);
+        final String columnId = columnKeys.key(column);
 
         column.extend(this);
-        column.setId(columnKeys.key(column));
+        column.setId(columnId);
         columnSet.add(column);
         addDataGenerator(column);
 
+        getHeader().addColumn(columnId);
+
+        if (getHeaderRowCount() > 0) {
+            // TODO Default header API to be added in a later patch
+            HeaderRow defaultHeader = getHeaderRow(0);
+            defaultHeader.getCell(columnId).setText(caption);
+        }
+
         return column;
     }
 
@@ -1762,6 +1839,130 @@ public class Grid<T> extends AbstractSingleSelect<T> implements HasComponents {
         return descriptionGenerator;
     }
 
+    //
+    // HEADER AND FOOTER
+    //
+
+    /**
+     * Returns the header row at the given index.
+     *
+     * @param rowIndex
+     *            the index of the row, where the topmost row has index zero
+     * @return the header row at the index
+     * @throws IndexOutOfBoundsException
+     *             if {@code rowIndex < 0 || rowIndex >= getHeaderRowCount()}
+     */
+    public HeaderRow getHeaderRow(int rowIndex) {
+        return getHeader().getRow(rowIndex);
+    }
+
+    /**
+     * Gets the number of rows in the header section.
+     *
+     * @return the number of header rows
+     */
+    public int getHeaderRowCount() {
+        return header.getRowCount();
+    }
+
+    /**
+     * Inserts a new row at the given position to the header section. Shifts the
+     * row currently at that position and any subsequent rows down (adds one to
+     * their indices). Inserting at {@link #getHeaderRowCount()} appends the row
+     * at the bottom of the header.
+     *
+     * @param index
+     *            the index at which to insert the row, where the topmost row
+     *            has index zero
+     * @return the inserted header row
+     * 
+     * @throws IndexOutOfBoundsException
+     *             if {@code rowIndex < 0 || rowIndex > getHeaderRowCount()}
+     * 
+     * @see #appendHeaderRow()
+     * @see #prependHeaderRow()
+     * @see #removeHeaderRow(HeaderRow)
+     * @see #removeHeaderRow(int)
+     */
+    public HeaderRow addHeaderRowAt(int index) {
+        return getHeader().addRowAt(index);
+    }
+
+    /**
+     * Adds a new row at the bottom of the header section.
+     *
+     * @return the appended header row
+     * 
+     * @see #prependHeaderRow()
+     * @see #addHeaderRowAt(int)
+     * @see #removeHeaderRow(HeaderRow)
+     * @see #removeHeaderRow(int)
+     */
+    public HeaderRow appendHeaderRow() {
+        return addHeaderRowAt(getHeaderRowCount());
+    }
+
+    /**
+     * Adds a new row at the top of the header section.
+     *
+     * @return the prepended header row
+     * 
+     * @see #appendHeaderRow()
+     * @see #addHeaderRowAt(int)
+     * @see #removeHeaderRow(HeaderRow)
+     * @see #removeHeaderRow(int)
+     */
+    public HeaderRow prependHeaderRow() {
+        return addHeaderRowAt(0);
+    }
+
+    /**
+     * Removes the given row from the header section.
+     *
+     * @param row
+     *            the header row to be removed, not null
+     *
+     * @throws IllegalArgumentException
+     *             if the header does not contain the row
+     * 
+     * @see #removeHeaderRow(int)
+     * @see #addHeaderRowAt(int)
+     * @see #appendHeaderRow()
+     * @see #prependHeaderRow()
+     */
+    public void removeHeaderRow(HeaderRow row) {
+        getHeader().removeRow(row);
+    }
+
+    /**
+     * Removes the row at the given position from the header section.
+     *
+     * @param rowIndex
+     *            the index of the row to remove, where the topmost row has
+     *            index zero
+     *
+     * @throws IndexOutOfBoundsException
+     *             if {@code rowIndex < 0 || rowIndex >= getHeaderRowCount()}
+     * 
+     * @see #removeHeaderRow(HeaderRow)
+     * @see #addHeaderRowAt(int)
+     * @see #appendHeaderRow()
+     * @see #prependHeaderRow()
+     */
+    public void removeHeaderRow(int rowIndex) {
+        getHeader().removeRow(rowIndex);
+    }
+
+    /**
+     * Returns the header section of this grid. The default header contains a
+     * single row displaying the column captions.
+     *
+     * @return the header section
+     */
+    protected Header getHeader() {
+        return header;
+    }
+
     /**
      * Registers a new column resize listener.
      *
diff --git a/server/src/main/java/com/vaadin/ui/components/grid/Header.java b/server/src/main/java/com/vaadin/ui/components/grid/Header.java
new file mode 100644 (file)
index 0000000..8348795
--- /dev/null
@@ -0,0 +1,57 @@
+/*
+ * 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 com.vaadin.ui.Grid;
+
+/**
+ * Represents the header section of a Grid.
+ */
+public abstract class Header extends StaticSection<Header.Row> {
+
+    public class Row extends StaticSection.StaticRow<Row.Cell>
+            implements Grid.HeaderRow {
+
+        public class Cell extends StaticSection.StaticCell implements
+                Grid.HeaderCell {
+            protected Cell() {
+                super(Row.this);
+            }
+        }
+
+        /**
+         * @param section
+         */
+        protected Row() {
+            super(Header.this);
+        }
+
+        @Override
+        protected Cell createCell() {
+            return new Cell();
+        }
+
+        @Override
+        protected String getCellTagName() {
+            return "th";
+        }
+    }
+
+    @Override
+    public Row createRow() {
+        return new Row();
+    }
+}
diff --git a/server/src/main/java/com/vaadin/ui/components/grid/StaticSection.java b/server/src/main/java/com/vaadin/ui/components/grid/StaticSection.java
new file mode 100644 (file)
index 0000000..dbd5749
--- /dev/null
@@ -0,0 +1,307 @@
+/*
+ * 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.io.Serializable;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+import com.vaadin.shared.ui.grid.SectionState;
+import com.vaadin.shared.ui.grid.SectionState.CellState;
+import com.vaadin.shared.ui.grid.SectionState.RowState;
+
+/**
+ * Represents the header or footer section of a Grid.
+ *
+ * @author Vaadin Ltd.
+ * 
+ * @param <ROW>
+ *            the type of the rows in the section
+ *
+ * @since 8.0
+ */
+public abstract class StaticSection<ROW extends StaticSection.StaticRow<?>>
+        implements Serializable {
+
+    /**
+     * Abstract base class for Grid header and footer rows.
+     *
+     * @param <CELL>
+     *            the type of the cells in the row
+     */
+    public abstract static class StaticRow<CELL extends StaticCell>
+            implements Serializable {
+
+        private RowState rowState = new RowState();
+        private StaticSection<?> section;
+        private Map<Object, CELL> cells = new LinkedHashMap<>();
+
+        /**
+         * Creates a new row belonging to the given section.
+         * 
+         * @param section
+         *            the section of the row
+         */
+        protected StaticRow(StaticSection<?> section) {
+            this.section = section;
+        }
+
+        /**
+         * Creates and returns a new instance of the cell type.
+         *
+         * @return the created cell
+         */
+        protected abstract CELL createCell();
+
+        /**
+         * Returns the declarative tag name used for the cells in this row.
+         * 
+         * @return the cell tag name
+         */
+        protected abstract String getCellTagName();
+
+        /**
+         * Adds a cell to this section, corresponding to the given column id.
+         * 
+         * @param columnId
+         *            the id of the column for which to add a cell
+         */
+        protected void addCell(String columnId) {
+            CELL cell = createCell();
+            cell.setColumnId(columnId);
+            cells.put(columnId, cell);
+            rowState.cells.put(columnId, cell.getCellState());
+        }
+
+        /**
+         * Removes the cell from this section that corresponds to the given
+         * column id. If there is no such cell, does nothing.
+         * 
+         * @param columnId
+         *            the id of the column from which to remove the cell
+         */
+        protected void removeCell(Object columnId) {
+            CELL cell = cells.remove(columnId);
+            if (cell != null) {
+                rowState.cells.remove(cell.getCellState());
+            }
+        }
+
+        /**
+         * Returns the shared state of this row.
+         * 
+         * @return the row state
+         */
+        protected RowState getRowState() {
+            return rowState;
+        }
+
+        /**
+         * Returns the cell in this section that corresponds to the given column
+         * id.
+         *
+         * @param columnId
+         *            the id of the column
+         * @return the cell for the given column or null if not found
+         */
+        public CELL getCell(String columnId) {
+            CELL cell = cells.get(columnId);
+            return cell;
+        }
+    }
+
+    /**
+     * A header or footer cell. Has a simple textual caption.
+     */
+    abstract static class StaticCell implements Serializable {
+
+        private CellState cellState = new CellState();
+        private StaticRow<?> row;
+
+        protected StaticCell(StaticRow<?> row) {
+            this.row = row;
+        }
+
+        void setColumnId(String id) {
+            cellState.columnId = id;
+        }
+
+        String getColumnId() {
+            return cellState.columnId;
+        }
+
+        /**
+         * Gets the row where this cell is.
+         *
+         * @return row for this cell
+         */
+        public StaticRow<?> getRow() {
+            return row;
+        }
+
+        /**
+         * Returns the shared state of this cell.
+         * 
+         * @return the cell state
+         */
+        protected CellState getCellState() {
+            return cellState;
+        }
+
+        /**
+         * Sets the textual caption of this cell.
+         *
+         * @param text
+         *            a plain text caption, not null
+         */
+        public void setText(String text) {
+            Objects.requireNonNull(text, "text cannot be null");
+            cellState.text = text;
+            row.section.markAsDirty();
+        }
+
+        /**
+         * Returns the textual caption of this cell.
+         *
+         * @return the plain text caption
+         */
+        public String getText() {
+            return cellState.text;
+        }
+    }
+
+    private List<ROW> rows = new ArrayList<>();
+
+    /**
+     * Creates a new row instance.
+     * 
+     * @return the new row
+     */
+    protected abstract ROW createRow();
+
+    /**
+     * Returns the shared state of this section.
+     * 
+     * @param markAsDirty
+     *            {@code true} to mark the state as modified, {@code false}
+     *            otherwise
+     * @return the section state
+     */
+    protected abstract SectionState getState(boolean markAsDirty);
+
+    /**
+     * Marks the state of this section as modified.
+     */
+    protected void markAsDirty() {
+        getState(true);
+    }
+
+    /**
+     * Adds a new row at the given index.
+     * 
+     * @param index
+     *            the index of the new row
+     * @return the added row
+     * @throws IndexOutOfBoundsException
+     *             if {@code index < 0 || index > getRowCount()}
+     */
+    public ROW addRowAt(int index) {
+        ROW row = createRow();
+        rows.add(index, row);
+        getState(true).rows.add(index, row.getRowState());
+        return row;
+    }
+
+    /**
+     * Removes the row at the given index.
+     * 
+     * @param index
+     *            the index of the row to remove
+     * @throws IndexOutOfBoundsException
+     *             if {@code index < 0 || index >= getRowCount()}
+     */
+    public void removeRow(int index) {
+        rows.remove(index);
+        getState(true).rows.remove(index);
+    }
+
+    /**
+     * Removes the given row from this section.
+     * 
+     * @param row
+     *            the row to remove, not null
+     * @throws IllegalArgumentException
+     *             if this section does not contain the row
+     */
+    public void removeRow(Object row) {
+        Objects.requireNonNull(row, "row cannot be null");
+        int index = rows.indexOf(row);
+        if (index < 0) {
+            throw new IllegalArgumentException(
+                    "Section does not contain the given row");
+        }
+        removeRow(index);
+    }
+
+    /**
+     * Returns the row at the given index.
+     * 
+     * @param index
+     *            the index of the row
+     * @return the row at the index
+     * @throws IndexOutOfBoundsException
+     *             if {@code index < 0 || index >= getRowCount()}
+     */
+    public ROW getRow(int index) {
+        return rows.get(index);
+    }
+
+    /**
+     * Returns the number of rows in this section.
+     *
+     * @return the number of rows
+     */
+    public int getRowCount() {
+        return rows.size();
+    }
+
+    /**
+     * Adds a cell corresponding to the given column id to this section.
+     *
+     * @param columnId
+     *            the id of the column for which to add a cell
+     */
+    public void addColumn(String columnId) {
+        for (ROW row : rows) {
+            row.addCell(columnId);
+        }
+    }
+
+    /**
+     * Removes the cell corresponding to the given column id.
+     *
+     * @param columnId
+     *            the id of the column whose cell to remove
+     */
+    public void removeColumn(String columnId) {
+        for (ROW row : rows) {
+            row.removeCell(columnId);
+        }
+    }
+}
diff --git a/server/src/test/java/com/vaadin/tests/server/component/grid/GridHeaderFooterTest.java b/server/src/test/java/com/vaadin/tests/server/component/grid/GridHeaderFooterTest.java
new file mode 100644 (file)
index 0000000..87c2632
--- /dev/null
@@ -0,0 +1,155 @@
+/*
+ * 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.tests.server.component.grid;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.fail;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import com.vaadin.ui.Grid;
+import com.vaadin.ui.Grid.Column;
+import com.vaadin.ui.Grid.HeaderRow;
+
+public class GridHeaderFooterTest {
+
+    private Grid<String> grid;
+    private Column<?, ?> column1, column2;
+
+    @Before
+    public void setUp() {
+        grid = new Grid<>();
+
+        column1 = grid.addColumn("First", s -> s.substring(0, 1));
+        column2 = grid.addColumn("Rest", s -> s.substring(1));
+    }
+
+    @Test
+    public void initialState_hasDefaultHeader() {
+        assertEquals(1, grid.getHeaderRowCount());
+        HeaderRow defaultHeader = grid.getHeaderRow(0);
+        assertEquals("First", defaultHeader.getCell(column1).getText());
+        assertEquals("Rest", defaultHeader.getCell(column2).getText());
+    }
+
+    @Test
+    public void initialState_defaultHeaderRemovable() {
+        grid.removeHeaderRow(0);
+        assertEquals(0, grid.getHeaderRowCount());
+    }
+
+    @Test
+    public void appendHeaderRow_addedToBottom() {
+        HeaderRow defaultRow = grid.getHeaderRow(0);
+        HeaderRow addedRow = grid.appendHeaderRow();
+
+        assertSame(defaultRow, grid.getHeaderRow(0));
+        assertSame(addedRow, grid.getHeaderRow(1));
+    }
+
+    @Test
+    public void prependHeaderRow_addedToTop() {
+        HeaderRow defaultRow = grid.getHeaderRow(0);
+        HeaderRow addedRow = grid.prependHeaderRow();
+
+        assertSame(addedRow, grid.getHeaderRow(0));
+        assertSame(defaultRow, grid.getHeaderRow(1));
+    }
+
+    @Test
+    public void addHeaderRowAtZero_addedToTop() {
+        HeaderRow defaultRow = grid.getHeaderRow(0);
+        HeaderRow addedRow = grid.addHeaderRowAt(0);
+
+        assertSame(addedRow, grid.getHeaderRow(0));
+        assertSame(defaultRow, grid.getHeaderRow(1));
+    }
+
+    @Test
+    public void addHeaderRowAtRowCount_addedToBottom() {
+        HeaderRow defaultRow = grid.getHeaderRow(0);
+        HeaderRow addedRow = grid.addHeaderRowAt(grid.getHeaderRowCount());
+
+        assertSame(defaultRow, grid.getHeaderRow(0));
+        assertSame(addedRow, grid.getHeaderRow(1));
+    }
+
+    @Test
+    public void removeExistingHeaderRow_removed() {
+        HeaderRow defaultRow = grid.getHeaderRow(0);
+        HeaderRow addedRow = grid.appendHeaderRow();
+
+        grid.removeHeaderRow(addedRow);
+
+        assertEquals(1, grid.getHeaderRowCount());
+        assertSame(defaultRow, grid.getHeaderRow(0));
+    }
+
+    @Test
+    public void removeDefaultHeaderRow_removed() {
+        HeaderRow defaultRow = grid.getHeaderRow(0);
+        HeaderRow addedRow = grid.appendHeaderRow();
+
+        grid.removeHeaderRow(defaultRow);
+
+        assertEquals(1, grid.getHeaderRowCount());
+        assertSame(addedRow, grid.getHeaderRow(0));
+    }
+
+    @Test(expected = IndexOutOfBoundsException.class)
+    public void getHeaderRowNegativeIndex_throws() {
+        grid.getHeaderRow(-1);
+    }
+
+    @Test(expected = IndexOutOfBoundsException.class)
+    public void getHeaderRowIndexTooLarge_throws() {
+        grid.appendHeaderRow();
+        grid.getHeaderRow(2);
+    }
+
+    @Test(expected = IndexOutOfBoundsException.class)
+    public void addHeaderRowAtNegativeIndex_throws() {
+        grid.addHeaderRowAt(-1);
+    }
+
+    @Test(expected = IndexOutOfBoundsException.class)
+    public void addHeaderRowAtIndexTooLarge_throws() {
+        grid.addHeaderRowAt(2);
+    }
+
+    @Test(expected = IndexOutOfBoundsException.class)
+    public void removeHeaderRowNegativeIndex_throws() {
+        grid.removeHeaderRow(-1);
+    }
+
+    @Test(expected = IndexOutOfBoundsException.class)
+    public void removeHeaderRowIndexTooLarge_throws() {
+        grid.removeHeaderRow(1);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void removeNonExistingHeaderRow_throws() {
+        HeaderRow row = grid.getHeaderRow(0);
+        try {
+            grid.removeHeaderRow(row);
+        } catch (Exception e) {
+            fail("unexpected exception: " + e);
+        }
+        grid.removeHeaderRow(row);
+    }
+}
index 4f480139d51958762b7ba84e5f3ec9664d9f5890..6c9631b59835c886a10c62c8b899b711ef1e72ca 100644 (file)
@@ -99,6 +99,9 @@ public class GridState extends AbstractSingleSelectState {
         primaryStyleName = "v-grid";
     }
 
+    /** The state of the header section. */
+    public SectionState header = new SectionState();
+
     /**
      * Column order in grid.
      */
diff --git a/shared/src/main/java/com/vaadin/shared/ui/grid/SectionState.java b/shared/src/main/java/com/vaadin/shared/ui/grid/SectionState.java
new file mode 100644 (file)
index 0000000..55a8df9
--- /dev/null
@@ -0,0 +1,57 @@
+/*
+ * 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.shared.ui.grid;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Shared state for Grid headers and footers.
+ *
+ * @author Vaadin Ltd
+ * @since 7.4
+ */
+public class SectionState implements Serializable {
+
+    /** The state of a header or footer row. */
+    public static class RowState implements Serializable {
+
+        /** The map from column ids to the cells in this row. */
+        public Map<String, CellState> cells = new HashMap<>();
+
+        /**
+         * Whether this row is the default header row. Always false for footer
+         * rows.
+         */
+        public boolean defaultHeader = false;
+    }
+
+    /** The state of a header or footer cell. */
+    public static class CellState implements Serializable {
+
+        /** The textual caption of this cell. */
+        public String text;
+
+        /** The id of the column that this cell belongs to. */
+        public String columnId;
+    }
+
+    /** The rows in this section. */
+    public List<RowState> rows = new ArrayList<>();
+}
index 67e64738508a3a0da7135a66e33abbe0f2432cd2..a07550a20ad0cf2333024ffe4052261f86f9180f 100644 (file)
@@ -2,6 +2,7 @@ package com.vaadin.tests.components.grid;
 
 import java.util.Arrays;
 
+import com.vaadin.annotations.Widgetset;
 import com.vaadin.server.VaadinRequest;
 import com.vaadin.tests.components.AbstractTestUI;
 import com.vaadin.tests.data.bean.Person;
@@ -11,6 +12,7 @@ import com.vaadin.ui.Label;
 import com.vaadin.ui.TextField;
 import com.vaadin.ui.renderers.NumberRenderer;
 
+@Widgetset("com.vaadin.DefaultWidgetSet")
 public class GridColumnResizing extends AbstractTestUI {
 
     @Override
index 3a0646ed273edf5ccded594d7ba53378966a57e5..bf0aa3ea5634645ff0604db9478d2c41ca211467 100644 (file)
@@ -33,6 +33,8 @@ import com.vaadin.ui.renderers.ProgressBarRenderer;
 @Widgetset("com.vaadin.DefaultWidgetSet")
 public class GridBasics extends AbstractTestUIWithLog {
 
+    public static final String[] COLUMN_CAPTIONS  = { "Column 0", "Column 1", "Column 2", "Row Number", "Date", "HTML String", "Big Random", "Small Random" };
+
     public static final String ROW_STYLE_GENERATOR_ROW_NUMBERS_FOR_3_OF_4 = "Row numbers for 3/4";
     public static final String ROW_STYLE_GENERATOR_NONE = "None";
     public static final String ROW_STYLE_GENERATOR_ROW_NUMBERS = "Row numbers";
@@ -128,21 +130,21 @@ public class GridBasics extends AbstractTestUIWithLog {
         grid = new Grid<>();
         grid.setItems(data);
 
-        grid.addColumn("Column 0",
+        grid.addColumn(COLUMN_CAPTIONS[0],
                 dataObj -> "(" + dataObj.getRowNumber() + ", 0)");
-        grid.addColumn("Column 1",
+        grid.addColumn(COLUMN_CAPTIONS[1],
                 dataObj -> "(" + dataObj.getRowNumber() + ", 1)");
-        grid.addColumn("Column 2",
+        grid.addColumn(COLUMN_CAPTIONS[2],
                 dataObj -> "(" + dataObj.getRowNumber() + ", 2)");
 
-        grid.addColumn("Row Number", DataObject::getRowNumber,
+        grid.addColumn(COLUMN_CAPTIONS[3], DataObject::getRowNumber,
                 new NumberRenderer());
-        grid.addColumn("Date", DataObject::getDate, new DateRenderer());
-        grid.addColumn("HTML String", DataObject::getHtmlString,
+        grid.addColumn(COLUMN_CAPTIONS[4], DataObject::getDate, new DateRenderer());
+        grid.addColumn(COLUMN_CAPTIONS[5], DataObject::getHtmlString,
                 new HtmlRenderer());
-        grid.addColumn("Big Random", DataObject::getBigRandom,
+        grid.addColumn(COLUMN_CAPTIONS[6], DataObject::getBigRandom,
                 new NumberRenderer());
-        grid.addColumn("Small Random", data -> data.getSmallRandom() / 5d,
+        grid.addColumn(COLUMN_CAPTIONS[7], data -> data.getSmallRandom() / 5d,
                 new ProgressBarRenderer());
 
         grid.addSelectionListener(e -> log("Selected: " + e.getValue()));
@@ -159,6 +161,7 @@ public class GridBasics extends AbstractTestUIWithLog {
         createSizeMenu(componentMenu.addItem("Size", null));
         createDetailsMenu(componentMenu.addItem("Details", null));
         createBodyMenu(componentMenu.addItem("Body rows", null));
+        createHeaderMenu(componentMenu.addItem("Header", null));
         return menu;
     }
 
@@ -293,6 +296,18 @@ public class GridBasics extends AbstractTestUIWithLog {
         });
     }
 
+    private void createHeaderMenu(MenuItem headerMenu) {
+        headerMenu.addItem("Append header row", menuItem -> {
+            grid.appendHeaderRow();
+        });
+        headerMenu.addItem("Prepend header row", menuItem -> {
+            grid.prependHeaderRow();
+        });
+        headerMenu.addItem("Remove first header row", menuItem -> {
+            grid.removeHeaderRow(0);
+        });
+    }
+
     /* DetailsGenerator related things */
 
     private void createDetailsMenu(MenuItem detailsMenu) {
diff --git a/uitest/src/test/java/com/vaadin/tests/components/grid/basics/GridHeaderFooterTest.java b/uitest/src/test/java/com/vaadin/tests/components/grid/basics/GridHeaderFooterTest.java
new file mode 100644 (file)
index 0000000..c1a4cfc
--- /dev/null
@@ -0,0 +1,69 @@
+/*
+ * 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.tests.components.grid.basics;
+
+import static org.junit.Assert.assertEquals;
+
+import java.util.List;
+
+import org.junit.Test;
+
+import com.vaadin.testbench.By;
+import com.vaadin.testbench.elements.GridElement.GridCellElement;
+
+public class GridHeaderFooterTest extends GridBasicsTest {
+
+    @Test
+    public void initialState_defaultHeaderPresent() {
+        assertEquals(1, getGridElement().getHeaderCount());
+
+        final String[] captions = GridBasics.COLUMN_CAPTIONS;
+        List<GridCellElement> headerCells = getGridElement().getHeaderCells(0);
+
+        assertEquals(captions.length, headerCells.size());
+        for (int i = 0; i < headerCells.size(); i++) {
+            assertText(captions[i], headerCells.get(i));
+        }
+    }
+
+    @Test
+    public void appendHeaderRow_addedToBottom() {
+        selectMenuPath("Component", "Header", "Append header row");
+
+        assertEquals(2, getGridElement().getHeaderCount());
+    }
+
+    @Test
+    public void prependHeaderRow_addedToBottom() {
+        selectMenuPath("Component", "Header", "Prepend header row");
+
+        assertEquals(2, getGridElement().getHeaderCount());
+    }
+
+    @Test
+    public void removeDefaultHeaderRow_noHeaderRows() {
+        selectMenuPath("Component", "Header", "Remove first header row");
+
+        assertEquals(0, getGridElement().getHeaderCount());
+    }
+
+    protected static void assertText(String expected, GridCellElement e) {
+        // TBE.getText returns "" if the element is scrolled out of view
+        String actual = e.findElement(By.tagName("div")).getAttribute(
+                "innerHTML");
+        assertEquals(expected, actual);
+    }
+}