From 680b7009d4f453dc8eec42975192094b2e0e8e2f Mon Sep 17 00:00:00 2001 From: =?utf8?q?Johannes=20Dahlstr=C3=B6m?= Date: Wed, 21 Sep 2016 13:08:16 +0300 Subject: [PATCH] Initial support for multiple headers in new Grid Change-Id: I7a3fa34749322451ab5cef4465d4d7c76029c097 --- .../client/connectors/grid/GridConnector.java | 51 ++- server/src/main/java/com/vaadin/ui/Grid.java | 211 +++++++++++- .../com/vaadin/ui/components/grid/Header.java | 57 ++++ .../ui/components/grid/StaticSection.java | 307 ++++++++++++++++++ .../component/grid/GridHeaderFooterTest.java | 155 +++++++++ .../com/vaadin/shared/ui/grid/GridState.java | 3 + .../vaadin/shared/ui/grid/SectionState.java | 57 ++++ .../components/grid/GridColumnResizing.java | 2 + .../components/grid/basics/GridBasics.java | 31 +- .../grid/basics/GridHeaderFooterTest.java | 69 ++++ 10 files changed, 925 insertions(+), 18 deletions(-) create mode 100644 server/src/main/java/com/vaadin/ui/components/grid/Header.java create mode 100644 server/src/main/java/com/vaadin/ui/components/grid/StaticSection.java create mode 100644 server/src/test/java/com/vaadin/tests/server/component/grid/GridHeaderFooterTest.java create mode 100644 shared/src/main/java/com/vaadin/shared/ui/grid/SectionState.java create mode 100644 uitest/src/test/java/com/vaadin/tests/components/grid/basics/GridHeaderFooterTest.java diff --git a/client/src/main/java/com/vaadin/client/connectors/grid/GridConnector.java b/client/src/main/java/com/vaadin/client/connectors/grid/GridConnector.java index ed07d1d3f1..0da4ec3bcc 100644 --- a/client/src/main/java/com/vaadin/client/connectors/grid/GridConnector.java +++ b/client/src/main/java/com/vaadin/client/connectors/grid/GridConnector.java @@ -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, String> columnToIdMap = new HashMap<>(); + private Map> idToColumn = new HashMap<>(); + /* Child component list for HasComponentsConnector */ private List childComponents; private SpaceSelectHandler 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 getWidget() { @@ -202,6 +219,28 @@ public class GridConnector layout(); } + @OnStateChange("header") + void updateHeader() { + final SectionState state = getState().header; + final Grid 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 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 diff --git a/server/src/main/java/com/vaadin/ui/Grid.java b/server/src/main/java/com/vaadin/ui/Grid.java index 94f8c9aee7..3777decbe9 100644 --- a/server/src/main/java/com/vaadin/ui/Grid.java +++ b/server/src/main/java/com/vaadin/ui/Grid.java @@ -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 extends AbstractSingleSelect implements HasComponents { if (rowKey != null) { item = getDataCommunicator().getKeyMapper().get(rowKey); } - fireEvent(new GridContextClickEvent(Grid.this, details, section, + fireEvent(new GridContextClickEvent<>(Grid.this, details, section, rowIndex, item, getColumn(columnId))); } @@ -824,6 +826,15 @@ public class Grid extends AbstractSingleSelect 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 extends AbstractSingleSelect 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> columnKeys = new KeyMapper<>(); private Set> columnSet = new LinkedHashSet<>(); private List>> sortOrder = new ArrayList<>(); @@ -1416,15 +1474,26 @@ public class Grid extends AbstractSingleSelect implements HasComponents { private StyleGenerator styleGenerator = item -> null; private DescriptionGenerator 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 extends AbstractSingleSelect implements HasComponents { * the value provider * @param renderer * the column value class - * @param - * the type of this grid * @param * the column value type * @@ -1467,13 +1534,23 @@ public class Grid extends AbstractSingleSelect implements HasComponents { public Column addColumn(String caption, Function valueProvider, AbstractRenderer renderer) { - Column column = new Column<>(caption, valueProvider, renderer); + final Column 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 extends AbstractSingleSelect 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 index 0000000000..8348795c10 --- /dev/null +++ b/server/src/main/java/com/vaadin/ui/components/grid/Header.java @@ -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 { + + public class Row extends StaticSection.StaticRow + 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 index 0000000000..dbd5749bd0 --- /dev/null +++ b/server/src/main/java/com/vaadin/ui/components/grid/StaticSection.java @@ -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 + * the type of the rows in the section + * + * @since 8.0 + */ +public abstract class StaticSection> + implements Serializable { + + /** + * Abstract base class for Grid header and footer rows. + * + * @param + * the type of the cells in the row + */ + public abstract static class StaticRow + implements Serializable { + + private RowState rowState = new RowState(); + private StaticSection section; + private Map 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 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 index 0000000000..87c2632b40 --- /dev/null +++ b/server/src/test/java/com/vaadin/tests/server/component/grid/GridHeaderFooterTest.java @@ -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 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); + } +} diff --git a/shared/src/main/java/com/vaadin/shared/ui/grid/GridState.java b/shared/src/main/java/com/vaadin/shared/ui/grid/GridState.java index 4f480139d5..6c9631b598 100644 --- a/shared/src/main/java/com/vaadin/shared/ui/grid/GridState.java +++ b/shared/src/main/java/com/vaadin/shared/ui/grid/GridState.java @@ -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 index 0000000000..55a8df99b8 --- /dev/null +++ b/shared/src/main/java/com/vaadin/shared/ui/grid/SectionState.java @@ -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 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 rows = new ArrayList<>(); +} diff --git a/uitest/src/main/java/com/vaadin/tests/components/grid/GridColumnResizing.java b/uitest/src/main/java/com/vaadin/tests/components/grid/GridColumnResizing.java index 67e6473850..a07550a20a 100644 --- a/uitest/src/main/java/com/vaadin/tests/components/grid/GridColumnResizing.java +++ b/uitest/src/main/java/com/vaadin/tests/components/grid/GridColumnResizing.java @@ -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 diff --git a/uitest/src/main/java/com/vaadin/tests/components/grid/basics/GridBasics.java b/uitest/src/main/java/com/vaadin/tests/components/grid/basics/GridBasics.java index 3a0646ed27..bf0aa3ea56 100644 --- a/uitest/src/main/java/com/vaadin/tests/components/grid/basics/GridBasics.java +++ b/uitest/src/main/java/com/vaadin/tests/components/grid/basics/GridBasics.java @@ -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 index 0000000000..c1a4cfc2b0 --- /dev/null +++ b/uitest/src/test/java/com/vaadin/tests/components/grid/basics/GridHeaderFooterTest.java @@ -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 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); + } +} -- 2.39.5