diff options
author | Denis <denis@vaadin.com> | 2016-12-13 15:46:29 +0200 |
---|---|---|
committer | Pekka Hyvönen <pekka@vaadin.com> | 2016-12-13 15:46:29 +0200 |
commit | a43fd9003a77f253a78b807d4ecadcd828b936a4 (patch) | |
tree | 0182ee00bba9dc69d9cacba367ac00666945390a | |
parent | 6d1abeb9fc1103af0a04dfd79d56b0f883de6f72 (diff) | |
download | vaadin-framework-a43fd9003a77f253a78b807d4ecadcd828b936a4.tar.gz vaadin-framework-a43fd9003a77f253a78b807d4ecadcd828b936a4.zip |
Provide declarative support for Grid. (#7961)
Fixes vaadin/framework8-issues#390
14 files changed, 1501 insertions, 64 deletions
diff --git a/all/src/main/templates/release-notes.html b/all/src/main/templates/release-notes.html index 1daed88508..70f8539e6d 100644 --- a/all/src/main/templates/release-notes.html +++ b/all/src/main/templates/release-notes.html @@ -176,11 +176,13 @@ <li><tt>required-indicator-visible</tt> attribute replaces the <tt>required</tt> attribute</li> <li><tt>placeholder</tt> attribute replaces the <tt>input-prompt</tt> attribute for input components</li> <li><tt>multi-select</tt> attribute is no longer used for select components</li> - <li><tt>v-option-group</tt> with attribute <tt>multi-select=true</tt> is replaced by <tt>v-check-box-group</tt></li> - <li><tt>v-option-group</tt> with attribute <tt>multi-select=false</tt> is replaced by <tt>v-radio-button-group</tt></li> + <li><tt>vaadin-option-group</tt> with attribute <tt>multi-select=true</tt> is replaced by <tt>v-check-box-group</tt></li> + <li><tt>vaadin-option-group</tt> with attribute <tt>multi-select=false</tt> is replaced by <tt>v-radio-button-group</tt></li> <li><tt>immediate</tt> attribute is not used for any component</li> <li><tt>read-only</tt> attribute is now only used for field components instead of all components</li> - <li><tt>v-upload</tt> has a new attribute <tt>immediate-mode</tt> that replaces the removed <tt>immediate</tt> attribue</li> + <li><tt>vaadin-upload</tt> has a new attribute <tt>immediate-mode</tt> that replaces the removed <tt>immediate</tt> attribue</li> + <li><tt>vaadin-grid</tt> column elements don't have <tt>property-id</tt> attribute anymore. Columns aren't addressed by a property anymore but they have an id. So there is <tt>column-id</tt> attribute instead</li> + <li><tt>vaadin-grid</tt> doesn't have <tt>readonly</tt> attribute anymore. It is replaced by <tt>selection-allowed</tt> attribute which has special meaning for a <tt>Grid</tt></li> </ul> <ul id="legacycomponents"><h4>Legacy components in the v7 compatiblity package <tt>com.vaadin.v7.ui</tt> available as a separate dependency</h4> <li><tt>Calendar</tt> - no replacement in 8</li> diff --git a/server/src/main/java/com/vaadin/ui/AbstractListing.java b/server/src/main/java/com/vaadin/ui/AbstractListing.java index de742e2145..0fa18bddbc 100644 --- a/server/src/main/java/com/vaadin/ui/AbstractListing.java +++ b/server/src/main/java/com/vaadin/ui/AbstractListing.java @@ -269,7 +269,26 @@ public abstract class AbstractListing<T> extends AbstractComponent { @Override public void writeDesign(Element design, DesignContext designContext) { super.writeDesign(design, designContext); + doWriteDesign(design, designContext); + } + /** + * Writes listing specific state into the given design. + * <p> + * This method is separated from {@link writeDesign(Element, DesignContext)} + * to be overridable in subclasses that need to replace this, but still must + * be able to call {@code super.writeDesign(...)}. + * + * @see #doReadDesign(Element, DesignContext) + * + * @param design + * The element to write the component state to. Any previous + * attributes or child nodes are <i>not</i> cleared. + * @param designContext + * The DesignContext instance used for writing the design + * + */ + protected void doWriteDesign(Element design, DesignContext designContext) { // Write options if warranted if (designContext.shouldWriteData(this)) { writeItems(design, designContext); @@ -330,6 +349,24 @@ public abstract class AbstractListing<T> extends AbstractComponent { @Override public void readDesign(Element design, DesignContext context) { super.readDesign(design, context); + doReadDesign(design, context); + } + + /** + * Reads the listing specific state from the given design. + * <p> + * This method is separated from {@link readDesign(Element, DesignContext)} + * to be overridable in subclasses that need to replace this, but still must + * be able to call {@code super.readDesign(...)}. + * + * @see #doWriteDesign(Element, DesignContext) + * + * @param design + * The element to obtain the state from + * @param context + * The DesignContext instance used for parsing the design + */ + protected void doReadDesign(Element design, DesignContext context) { Attributes attr = design.attributes(); if (attr.hasKey("readonly")) { setReadOnly(DesignAttributeHandler.readAttribute("readonly", attr, @@ -441,7 +478,7 @@ public abstract class AbstractListing<T> extends AbstractComponent { * <p> * Default implementation delegates a call to {@code item.toString()}. * - * @see #serializeDeclarativeRepresentation(Object) + * @see #deserializeDeclarativeRepresentation(String) * * @param item * a data item diff --git a/server/src/main/java/com/vaadin/ui/DeclarativeValueProvider.java b/server/src/main/java/com/vaadin/ui/DeclarativeValueProvider.java new file mode 100644 index 0000000000..9e2975b637 --- /dev/null +++ b/server/src/main/java/com/vaadin/ui/DeclarativeValueProvider.java @@ -0,0 +1,52 @@ +/* + * 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; + +import java.util.IdentityHashMap; +import java.util.Map; + +import com.vaadin.server.SerializableFunction; + +/** + * Value provider class for declarative support. + * <p> + * Provides a straightforward mapping between an item and its value. + * + * @param <T> + * item type + */ +class DeclarativeValueProvider<T> implements SerializableFunction<T, String> { + + private final Map<T, String> values = new IdentityHashMap<>(); + + @Override + public String apply(T t) { + return values.get(t); + } + + /** + * Sets a {@code value} for the item {@code t}. + * + * @param t + * a data item + * @param value + * a value for the item {@code t} + */ + void addValue(T t, String value) { + values.put(t, value); + } + +} diff --git a/server/src/main/java/com/vaadin/ui/Grid.java b/server/src/main/java/com/vaadin/ui/Grid.java index 4e0b8b7457..60bd9abcaf 100644 --- a/server/src/main/java/com/vaadin/ui/Grid.java +++ b/server/src/main/java/com/vaadin/ui/Grid.java @@ -30,7 +30,9 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; +import java.util.UUID; import java.util.function.BiFunction; import java.util.function.BinaryOperator; import java.util.function.Consumer; @@ -38,7 +40,9 @@ import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; +import org.jsoup.nodes.Attributes; import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; import com.vaadin.data.Binder; import com.vaadin.data.BinderValidationStatus; @@ -57,6 +61,7 @@ import com.vaadin.server.SerializableComparator; import com.vaadin.server.SerializableFunction; import com.vaadin.server.data.DataCommunicator; import com.vaadin.server.data.DataProvider; +import com.vaadin.server.data.Query; import com.vaadin.server.data.SortOrder; import com.vaadin.shared.MouseEventDetails; import com.vaadin.shared.Registration; @@ -74,6 +79,7 @@ import com.vaadin.shared.ui.grid.HeightMode; import com.vaadin.shared.ui.grid.SectionState; import com.vaadin.shared.util.SharedUtil; import com.vaadin.ui.Grid.FooterRow; +import com.vaadin.ui.Grid.SelectionMode; import com.vaadin.ui.components.grid.AbstractSelectionModel; import com.vaadin.ui.components.grid.EditorImpl; import com.vaadin.ui.components.grid.Footer; @@ -82,8 +88,12 @@ import com.vaadin.ui.components.grid.Header.Row; import com.vaadin.ui.components.grid.MultiSelectionModelImpl; import com.vaadin.ui.components.grid.NoSelectionModel; import com.vaadin.ui.components.grid.SingleSelectionModelImpl; +import com.vaadin.ui.declarative.DesignAttributeHandler; import com.vaadin.ui.declarative.DesignContext; +import com.vaadin.ui.declarative.DesignException; +import com.vaadin.ui.declarative.DesignFormatter; import com.vaadin.ui.renderers.AbstractRenderer; +import com.vaadin.ui.renderers.HtmlRenderer; import com.vaadin.ui.renderers.Renderer; import com.vaadin.ui.renderers.TextRenderer; import com.vaadin.util.ReflectTools; @@ -1780,6 +1790,136 @@ public class Grid<T> extends AbstractListing<T> "Column is no longer attached to a grid."); } } + + /** + * Writes the design attributes for this column into given element. + * + * @since 7.5.0 + * + * @param element + * Element to write attributes into + * + * @param designContext + * the design context + */ + protected void writeDesign(Element element, + DesignContext designContext) { + Attributes attributes = element.attributes(); + + ColumnState defaultState = new ColumnState(); + + DesignAttributeHandler.writeAttribute("column-id", attributes, + getId(), null, String.class, designContext); + + // Sortable is a special attribute that depends on the data + // provider. + DesignAttributeHandler.writeAttribute("sortable", attributes, + isSortable(), null, boolean.class, designContext); + DesignAttributeHandler.writeAttribute("editable", attributes, + isEditable(), defaultState.editable, boolean.class, + designContext); + DesignAttributeHandler.writeAttribute("resizable", attributes, + isResizable(), defaultState.resizable, boolean.class, + designContext); + + DesignAttributeHandler.writeAttribute("hidable", attributes, + isHidable(), defaultState.hidable, boolean.class, + designContext); + DesignAttributeHandler.writeAttribute("hidden", attributes, + isHidden(), defaultState.hidden, boolean.class, + designContext); + DesignAttributeHandler.writeAttribute("hiding-toggle-caption", + attributes, getHidingToggleCaption(), + defaultState.hidingToggleCaption, String.class, + designContext); + + DesignAttributeHandler.writeAttribute("width", attributes, + getWidth(), defaultState.width, Double.class, + designContext); + DesignAttributeHandler.writeAttribute("min-width", attributes, + getMinimumWidth(), defaultState.minWidth, Double.class, + designContext); + DesignAttributeHandler.writeAttribute("max-width", attributes, + getMaximumWidth(), defaultState.maxWidth, Double.class, + designContext); + DesignAttributeHandler.writeAttribute("expand", attributes, + getExpandRatio(), defaultState.expandRatio, Integer.class, + designContext); + } + + /** + * Reads the design attributes for this column from given element. + * + * @since 7.5.0 + * @param design + * Element to read attributes from + * @param designContext + * the design context + */ + protected void readDesign(Element design, DesignContext designContext) { + Attributes attributes = design.attributes(); + + if (design.hasAttr("sortable")) { + setSortable(DesignAttributeHandler.readAttribute("sortable", + attributes, boolean.class)); + } else { + setSortable(false); + } + if (design.hasAttr("editable")) { + /* + * This is a fake editor just to have something (otherwise + * "setEditable" throws an exception. + * + * Let's use TextField here because we support only Strings as + * inline data type. It will work incorrectly for other types + * but we don't support them anyway. + */ + setEditorComponentGenerator(item -> new TextField( + Optional.ofNullable(valueProvider.apply(item)) + .map(Object::toString).orElse(""))); + setEditable(DesignAttributeHandler.readAttribute("editable", + attributes, boolean.class)); + } + if (design.hasAttr("resizable")) { + setResizable(DesignAttributeHandler.readAttribute("resizable", + attributes, boolean.class)); + } + + if (design.hasAttr("hidable")) { + setHidable(DesignAttributeHandler.readAttribute("hidable", + attributes, boolean.class)); + } + if (design.hasAttr("hidden")) { + setHidden(DesignAttributeHandler.readAttribute("hidden", + attributes, boolean.class)); + } + if (design.hasAttr("hiding-toggle-caption")) { + setHidingToggleCaption(DesignAttributeHandler.readAttribute( + "hiding-toggle-caption", attributes, String.class)); + } + + // Read size info where necessary. + if (design.hasAttr("width")) { + setWidth(DesignAttributeHandler.readAttribute("width", + attributes, Double.class)); + } + if (design.hasAttr("min-width")) { + setMinimumWidth(DesignAttributeHandler + .readAttribute("min-width", attributes, Double.class)); + } + if (design.hasAttr("max-width")) { + setMaximumWidth(DesignAttributeHandler + .readAttribute("max-width", attributes, Double.class)); + } + if (design.hasAttr("expand")) { + if (design.attr("expand").isEmpty()) { + setExpandRatio(1); + } else { + setExpandRatio(DesignAttributeHandler.readAttribute( + "expand", attributes, Integer.class)); + } + } + } } /** @@ -3307,14 +3447,7 @@ public class Grid<T> extends AbstractListing<T> } @Override - protected Element writeItem(Element design, T item, DesignContext context) { - // TODO see vaadin/framework8-issues#390 - return null; - } - - @Override protected List<T> readItems(Element design, DesignContext context) { - // TODO see vaadin/framework8-issues#390 return Collections.emptyList(); } @@ -3328,4 +3461,224 @@ public class Grid<T> extends AbstractListing<T> internalSetDataProvider(dataProvider); } + @Override + protected void doReadDesign(Element design, DesignContext context) { + Attributes attrs = design.attributes(); + if (attrs.hasKey("selection-mode")) { + setSelectionMode(DesignAttributeHandler.readAttribute( + "selection-mode", attrs, SelectionMode.class)); + } + Attributes attr = design.attributes(); + if (attr.hasKey("selection-allowed")) { + setReadOnly(DesignAttributeHandler + .readAttribute("selection-allowed", attr, Boolean.class)); + } + + if (attrs.hasKey("rows")) { + setHeightByRows(DesignAttributeHandler.readAttribute("rows", attrs, + double.class)); + } + + readStructure(design, context); + + // Read frozen columns after columns are read. + if (attrs.hasKey("frozen-columns")) { + setFrozenColumnCount(DesignAttributeHandler + .readAttribute("frozen-columns", attrs, int.class)); + } + } + + @Override + protected void doWriteDesign(Element design, DesignContext designContext) { + Attributes attr = design.attributes(); + DesignAttributeHandler.writeAttribute("selection-allowed", attr, + isReadOnly(), false, Boolean.class, designContext); + + Attributes attrs = design.attributes(); + Grid<?> defaultInstance = designContext.getDefaultInstance(this); + + DesignAttributeHandler.writeAttribute("frozen-columns", attrs, + getFrozenColumnCount(), defaultInstance.getFrozenColumnCount(), + int.class, designContext); + + if (HeightMode.ROW.equals(getHeightMode())) { + DesignAttributeHandler.writeAttribute("rows", attrs, + getHeightByRows(), defaultInstance.getHeightByRows(), + double.class, designContext); + } + + SelectionMode mode = getSelectionMode(); + + if (mode != null) { + DesignAttributeHandler.writeAttribute("selection-mode", attrs, mode, + SelectionMode.SINGLE, SelectionMode.class, designContext); + } + + writeStructure(design, designContext); + } + + @Override + protected T deserializeDeclarativeRepresentation(String item) { + if (item == null) { + return super.deserializeDeclarativeRepresentation( + new String(UUID.randomUUID().toString())); + } + return super.deserializeDeclarativeRepresentation(new String(item)); + } + + @Override + protected boolean isReadOnly() { + SelectionMode selectionMode = getSelectionMode(); + if (SelectionMode.SINGLE.equals(selectionMode)) { + return asSingleSelect().isReadOnly(); + } else if (SelectionMode.MULTI.equals(selectionMode)) { + return asMultiSelect().isReadOnly(); + } + return false; + } + + @Override + protected void setReadOnly(boolean readOnly) { + SelectionMode selectionMode = getSelectionMode(); + if (SelectionMode.SINGLE.equals(selectionMode)) { + asSingleSelect().setReadOnly(readOnly); + } else if (SelectionMode.MULTI.equals(selectionMode)) { + asMultiSelect().setReadOnly(readOnly); + } + } + + private void readStructure(Element design, DesignContext context) { + if (design.children().isEmpty()) { + return; + } + if (design.children().size() > 1 + || !design.child(0).tagName().equals("table")) { + throw new DesignException( + "Grid needs to have a table element as its only child"); + } + Element table = design.child(0); + + Elements colgroups = table.getElementsByTag("colgroup"); + if (colgroups.size() != 1) { + throw new DesignException( + "Table element in declarative Grid needs to have a" + + " colgroup defining the columns used in Grid"); + } + + List<DeclarativeValueProvider<T>> providers = new ArrayList<>(); + for (Element col : colgroups.get(0).getElementsByTag("col")) { + String id = DesignAttributeHandler.readAttribute("column-id", + col.attributes(), null, String.class); + Column<T, String> column; + DeclarativeValueProvider<T> provider = new DeclarativeValueProvider<>(); + if (id != null) { + column = addColumn(id, provider, new HtmlRenderer()); + } else { + column = addColumn(provider, new HtmlRenderer()); + } + providers.add(provider); + column.readDesign(col, context); + } + + for (Element child : table.children()) { + if (child.tagName().equals("thead")) { + getHeader().readDesign(child, context); + } else if (child.tagName().equals("tbody")) { + readData(child, providers); + } else if (child.tagName().equals("tfoot")) { + getFooter().readDesign(child, context); + } + } + } + + private void readData(Element body, + List<DeclarativeValueProvider<T>> providers) { + getSelectionModel().deselectAll(); + List<T> items = new ArrayList<>(); + for (Element row : body.children()) { + T item = deserializeDeclarativeRepresentation(row.attr("item")); + if (row.hasAttr("selected")) { + getSelectionModel().select(item); + } + Elements cells = row.children(); + int i = 0; + for (Element cell : cells) { + providers.get(i).addValue(item, cell.html()); + i++; + } + } + + setItems(items); + } + + private void writeStructure(Element design, DesignContext designContext) { + if (getColumns().isEmpty()) { + return; + } + Element tableElement = design.appendElement("table"); + Element colGroup = tableElement.appendElement("colgroup"); + + getColumns().forEach(column -> column + .writeDesign(colGroup.appendElement("col"), designContext)); + + // Always write thead. Reads correctly when there no header rows + getHeader().writeDesign(tableElement.appendElement("thead"), + designContext); + + if (designContext.shouldWriteData(this)) { + Element bodyElement = tableElement.appendElement("tbody"); + getDataProvider().fetch(new Query<>()).forEach( + item -> writeRow(bodyElement, item, designContext)); + } + + if (getFooter().getRowCount() > 0) { + getFooter().writeDesign(tableElement.appendElement("tfoot"), + designContext); + } + } + + private void writeRow(Element container, T item, DesignContext context) { + Element tableRow = container.appendElement("tr"); + tableRow.attr("item", serializeDeclarativeRepresentation(item)); + if (getSelectionModel().isSelected(item)) { + tableRow.attr("selected", ""); + } + for (Column<T, ?> column : getColumns()) { + Object value = column.valueProvider.apply(item); + tableRow.appendElement("td") + .append((Optional.ofNullable(value).map(Object::toString) + .map(DesignFormatter::encodeForTextNode) + .orElse(""))); + } + } + + private SelectionMode getSelectionMode() { + GridSelectionModel<T> selectionModel = getSelectionModel(); + SelectionMode mode = null; + if (selectionModel.getClass().equals(SingleSelectionModelImpl.class)) { + mode = SelectionMode.SINGLE; + } else if (selectionModel.getClass() + .equals(MultiSelectionModelImpl.class)) { + mode = SelectionMode.MULTI; + } else if (selectionModel.getClass().equals(NoSelectionModel.class)) { + mode = SelectionMode.NONE; + } + return mode; + } + + @Override + protected Collection<String> getCustomAttributes() { + Collection<String> result = super.getCustomAttributes(); + // "rename" for frozen column count + result.add("frozen-column-count"); + result.add("frozen-columns"); + // "rename" for height-mode + result.add("height-by-rows"); + result.add("rows"); + // add a selection-mode attribute + result.add("selection-mode"); + + return result; + } + } diff --git a/server/src/main/java/com/vaadin/ui/components/grid/Footer.java b/server/src/main/java/com/vaadin/ui/components/grid/Footer.java index 0da179a2c0..7d4f45cd1e 100644 --- a/server/src/main/java/com/vaadin/ui/components/grid/Footer.java +++ b/server/src/main/java/com/vaadin/ui/components/grid/Footer.java @@ -15,12 +15,12 @@ */ package com.vaadin.ui.components.grid; -import com.vaadin.ui.Grid; - import java.util.Arrays; import java.util.HashSet; import java.util.Set; +import com.vaadin.ui.Grid; + /** * Represents the footer section of a Grid. * @@ -67,12 +67,14 @@ public abstract class Footer extends StaticSection<Footer.Row> { } /** - * Merges column cells in the row. Original cells are hidden, and new merged cell is shown instead. - * The cell has a width of all merged cells together, inherits styles of the first merged cell - * but has empty caption. + * Merges column cells in the row. Original cells are hidden, and new + * merged cell is shown instead. The cell has a width of all merged + * cells together, inherits styles of the first merged cell but has + * empty caption. * * @param cellsToMerge - * the cells which should be merged. The cells should not be merged to any other cell set. + * the cells which should be merged. The cells should not be + * merged to any other cell set. * @return the remaining visible cell after the merge * * @see #join(Grid.FooterCell...) @@ -97,12 +99,14 @@ public abstract class Footer extends StaticSection<Footer.Row> { } /** - * Merges column cells in the row. Original cells are hidden, and new merged cell is shown instead. - * The cell has a width of all merged cells together, inherits styles of the first merged cell - * but has empty caption. + * Merges column cells in the row. Original cells are hidden, and new + * merged cell is shown instead. The cell has a width of all merged + * cells together, inherits styles of the first merged cell but has + * empty caption. * * @param cellsToMerge - * the cells which should be merged. The cells should not be merged to any other cell set. + * the cells which should be merged. The cells should not be + * merged to any other cell set. * @return the remaining visible cell after the merge * * @see #join(Set) @@ -110,15 +114,16 @@ public abstract class Footer extends StaticSection<Footer.Row> { */ @Override public Grid.FooterCell join(Grid.FooterCell... cellsToMerge) { - Set<Grid.FooterCell> footerCells = new HashSet<>(Arrays.asList(cellsToMerge)); + Set<Grid.FooterCell> footerCells = new HashSet<>( + Arrays.asList(cellsToMerge)); return join(footerCells); } - } @Override public Row createRow() { return new Row(); } + } 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 index 5236a200fa..d1d369a9c8 100644 --- a/server/src/main/java/com/vaadin/ui/components/grid/Header.java +++ b/server/src/main/java/com/vaadin/ui/components/grid/Header.java @@ -15,12 +15,16 @@ */ package com.vaadin.ui.components.grid; -import com.vaadin.ui.Grid; - import java.util.Arrays; import java.util.HashSet; import java.util.Set; +import org.jsoup.nodes.Element; + +import com.vaadin.ui.Grid; +import com.vaadin.ui.declarative.DesignAttributeHandler; +import com.vaadin.ui.declarative.DesignContext; + /** * Represents the header section of a Grid. * @@ -39,8 +43,8 @@ public abstract class Header extends StaticSection<Header.Row> { /** * A cell in a Grid header row. */ - public class Cell extends StaticSection.StaticCell implements - Grid.HeaderCell { + public class Cell extends StaticSection.StaticCell + implements Grid.HeaderCell { /** * Creates a new header cell. */ @@ -87,12 +91,14 @@ public abstract class Header extends StaticSection<Header.Row> { } /** - * Merges column cells in the row. Original cells are hidden, and new merged cell is shown instead. - * The cell has a width of all merged cells together, inherits styles of the first merged cell - * but has empty caption. + * Merges column cells in the row. Original cells are hidden, and new + * merged cell is shown instead. The cell has a width of all merged + * cells together, inherits styles of the first merged cell but has + * empty caption. * * @param cellsToMerge - * the cells which should be merged. The cells should not be merged to any other cell set. + * the cells which should be merged. The cells should not be + * merged to any other cell set. * @return the remaining visible cell after the merge * * @see #join(Grid.HeaderCell...) @@ -117,12 +123,14 @@ public abstract class Header extends StaticSection<Header.Row> { } /** - * Merges column cells in the row. Original cells are hidden, and new merged cell is shown instead. - * The cell has a width of all merged cells together, inherits styles of the first merged cell - * but has empty caption. + * Merges column cells in the row. Original cells are hidden, and new + * merged cell is shown instead. The cell has a width of all merged + * cells together, inherits styles of the first merged cell but has + * empty caption. * * @param cellsToMerge - * the cells which should be merged. The cells should not be merged to any other cell set. + * the cells which should be merged. The cells should not be + * merged to any other cell set. * @return the remaining visible cell after the merge * * @see #join(Set) @@ -130,10 +138,34 @@ public abstract class Header extends StaticSection<Header.Row> { */ @Override public Grid.HeaderCell join(Grid.HeaderCell... cellsToMerge) { - Set<Grid.HeaderCell> headerCells = new HashSet<>(Arrays.asList(cellsToMerge)); + Set<Grid.HeaderCell> headerCells = new HashSet<>( + Arrays.asList(cellsToMerge)); return join(headerCells); } + @Override + protected void readDesign(Element trElement, + DesignContext designContext) { + super.readDesign(trElement, designContext); + + boolean defaultRow = DesignAttributeHandler.readAttribute("default", + trElement.attributes(), false, boolean.class); + if (defaultRow) { + setDefault(true); + } + } + + @Override + protected void writeDesign(Element trElement, + DesignContext designContext) { + super.writeDesign(trElement, designContext); + + if (isDefault()) { + DesignAttributeHandler.writeAttribute("default", + trElement.attributes(), true, null, boolean.class, + designContext); + } + } } @Override @@ -156,9 +188,7 @@ public abstract class Header extends StaticSection<Header.Row> { * @return the default row, or {@code null} if there is no default row */ public Row getDefaultRow() { - return getRows().stream() - .filter(Row::isDefault) - .findAny().orElse(null); + return getRows().stream().filter(Row::isDefault).findAny().orElse(null); } /** @@ -185,4 +215,5 @@ public abstract class Header extends StaticSection<Header.Row> { markAsDirty(); } + } diff --git a/server/src/main/java/com/vaadin/ui/components/grid/MultiSelectionModelImpl.java b/server/src/main/java/com/vaadin/ui/components/grid/MultiSelectionModelImpl.java index ba17315b07..fa25ff677c 100644 --- a/server/src/main/java/com/vaadin/ui/components/grid/MultiSelectionModelImpl.java +++ b/server/src/main/java/com/vaadin/ui/components/grid/MultiSelectionModelImpl.java @@ -316,12 +316,12 @@ public class MultiSelectionModelImpl<T> extends AbstractSelectionModel<T> @Override public void setReadOnly(boolean readOnly) { - getState().selectionAllowed = readOnly; + getState().selectionAllowed = !readOnly; } @Override public boolean isReadOnly() { - return isUserSelectionAllowed(); + return !isUserSelectionAllowed(); } @Override diff --git a/server/src/main/java/com/vaadin/ui/components/grid/SingleSelectionModelImpl.java b/server/src/main/java/com/vaadin/ui/components/grid/SingleSelectionModelImpl.java index 108ca6669e..8487ea87ef 100644 --- a/server/src/main/java/com/vaadin/ui/components/grid/SingleSelectionModelImpl.java +++ b/server/src/main/java/com/vaadin/ui/components/grid/SingleSelectionModelImpl.java @@ -269,12 +269,12 @@ public class SingleSelectionModelImpl<T> extends AbstractSelectionModel<T> @Override public void setReadOnly(boolean readOnly) { - getState().selectionAllowed = readOnly; + getState().selectionAllowed = !readOnly; } @Override public boolean isReadOnly() { - return isUserSelectionAllowed(); + return !isUserSelectionAllowed(); } }; } 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 index 5c7d682b17..3edb4fecc9 100644 --- a/server/src/main/java/com/vaadin/ui/components/grid/StaticSection.java +++ b/server/src/main/java/com/vaadin/ui/components/grid/StaticSection.java @@ -19,12 +19,21 @@ import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Objects; +import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; import com.vaadin.shared.ui.grid.GridStaticCellType; import com.vaadin.shared.ui.grid.SectionState; @@ -33,6 +42,10 @@ import com.vaadin.shared.ui.grid.SectionState.RowState; import com.vaadin.ui.Component; import com.vaadin.ui.Grid; import com.vaadin.ui.Grid.Column; +import com.vaadin.ui.declarative.DesignAttributeHandler; +import com.vaadin.ui.declarative.DesignContext; +import com.vaadin.ui.declarative.DesignException; +import com.vaadin.ui.declarative.DesignFormatter; /** * Represents the header or footer section of a Grid. @@ -108,10 +121,11 @@ public abstract class StaticSection<ROW extends StaticSection.StaticRow<?>> CELL cell = cells.remove(columnId); if (cell != null) { rowState.cells.remove(columnId); - for (Iterator<Set<String>> iterator = rowState.cellGroups.values().iterator(); iterator.hasNext(); ) { + for (Iterator<Set<String>> iterator = rowState.cellGroups + .values().iterator(); iterator.hasNext();) { Set<String> group = iterator.next(); group.remove(columnId); - if(group.size() < 2) { + if (group.size() < 2) { iterator.remove(); } } @@ -147,6 +161,143 @@ public abstract class StaticSection<ROW extends StaticSection.StaticRow<?>> return cell; } + /** + * Reads the declarative design from the given table row element. + * + * @since 7.5.0 + * @param trElement + * Element to read design from + * @param designContext + * the design context + * @throws DesignException + * if the given table row contains unexpected children + */ + protected void readDesign(Element trElement, + DesignContext designContext) throws DesignException { + Elements cellElements = trElement.children(); + for (int i = 0; i < cellElements.size(); i++) { + Element element = cellElements.get(i); + if (!element.tagName().equals(getCellTagName())) { + throw new DesignException( + "Unexpected element in tr while expecting " + + getCellTagName() + ": " + + element.tagName()); + } + + int colspan = DesignAttributeHandler.readAttribute("colspan", + element.attributes(), 1, int.class); + + String columnIdsString = DesignAttributeHandler.readAttribute( + "column-ids", element.attributes(), "", String.class); + if (columnIdsString.trim().isEmpty()) { + throw new DesignException( + "Unexpected 'column-ids' attribute value '" + + columnIdsString + + "'. It cannot be empty and must " + + "be comma separated column identifiers"); + } + String[] columnIds = columnIdsString.split(","); + if (columnIds.length != colspan) { + throw new DesignException( + "Unexpected 'colspan' attribute value '" + colspan + + "' whereas there is " + columnIds.length + + " column identifiers specified : '" + + columnIdsString + "'"); + } + + Stream.of(columnIds).forEach(this::addCell); + + Stream<String> idsStream = Stream.of(columnIds); + if (colspan > 1) { + CELL newCell = createCell(); + addMergedCell(createCell(), + idsStream.collect(Collectors.toSet())); + newCell.readDesign(element, designContext); + } else { + idsStream.map(this::getCell).forEach( + cell -> cell.readDesign(element, designContext)); + } + } + } + + /** + * Writes the declarative design to the given table row element. + * + * @since 7.5.0 + * @param trElement + * Element to write design to + * @param designContext + * the design context + */ + protected void writeDesign(Element trElement, + DesignContext designContext) { + Set<String> visited = new HashSet<>(); + for (Entry<String, CELL> entry : cells.entrySet()) { + if (visited.contains(entry.getKey())) { + continue; + } + visited.add(entry.getKey()); + + Element cellElement = trElement.appendElement(getCellTagName()); + + Optional<Entry<CellState, Set<String>>> groupCell = getRowState().cellGroups + .entrySet().stream().filter(groupEntry -> groupEntry + .getValue().contains(entry.getKey())) + .findFirst(); + Stream<String> columnIds = Stream.of(entry.getKey()); + if (groupCell.isPresent()) { + Set<String> orderedSet = new LinkedHashSet<>( + cells.keySet()); + orderedSet.retainAll(groupCell.get().getValue()); + columnIds = orderedSet.stream(); + visited.addAll(orderedSet); + cellElement.attr("colspan", "" + orderedSet.size()); + writeCellState(cellElement, designContext, + groupCell.get().getKey()); + } else { + writeCellState(cellElement, designContext, + entry.getValue().getCellState()); + } + cellElement.attr("column-ids", + columnIds.collect(Collectors.joining(","))); + } + } + + /** + * + * Writes declarative design for the cell using its {@code state} to the + * given table cell element. + * <p> + * The method is used instead of StaticCell::writeDesign because + * sometimes there is no a reference to the cell which should be written + * (merged cell) but only its state is available (the cell is virtual + * and is not stored). + * + * @param cellElement + * Element to write design to + * @param context + * the design context + * @param state + * a cell state + */ + protected void writeCellState(Element cellElement, + DesignContext context, CellState state) { + switch (state.type) { + case TEXT: + cellElement.attr("plain-text", true); + cellElement + .appendText(Optional.ofNullable(state.text).orElse("")); + break; + case HTML: + cellElement.append(Optional.ofNullable(state.html).orElse("")); + break; + case WIDGET: + cellElement.appendChild( + context.createElement((Component) state.connector)); + break; + } + } + void detach() { for (CELL cell : cells.values()) { cell.detach(); @@ -299,6 +450,31 @@ public abstract class StaticSection<ROW extends StaticSection.StaticRow<?>> return cellState.type; } + /** + * Reads the declarative design from the given table cell element. + * + * @since 7.5.0 + * @param cellElement + * Element to read design from + * @param designContext + * the design context + */ + protected void readDesign(Element cellElement, + DesignContext designContext) { + if (!cellElement.hasAttr("plain-text")) { + if (cellElement.children().size() > 0 + && cellElement.child(0).tagName().contains("-")) { + setComponent( + designContext.readDesign(cellElement.child(0))); + } else { + setHtml(cellElement.html()); + } + } else { + // text – need to unescape HTML entities + setText(DesignFormatter.decodeFromTextNode(cellElement.html())); + } + } + private void removeComponentIfPresent() { Component component = (Component) cellState.connector; if (component != null) { @@ -441,6 +617,48 @@ public abstract class StaticSection<ROW extends StaticSection.StaticRow<?>> } /** + * Writes the declarative design to the given table section element. + * + * @param tableSectionElement + * Element to write design to + * @param designContext + * the design context + */ + public void writeDesign(Element tableSectionElement, + DesignContext designContext) { + for (ROW row : getRows()) { + Element tr = tableSectionElement.appendElement("tr"); + row.writeDesign(tr, designContext); + } + } + + /** + * Reads the declarative design from the given table section element. + * + * @since 7.5.0 + * @param tableSectionElement + * Element to read design from + * @param designContext + * the design context + * @throws DesignException + * if the table section contains unexpected children + */ + public void readDesign(Element tableSectionElement, + DesignContext designContext) throws DesignException { + while (getRowCount() > 0) { + removeRow(0); + } + + for (Element row : tableSectionElement.children()) { + if (!row.tagName().equals("tr")) { + throw new DesignException("Unexpected element in " + + tableSectionElement.tagName() + ": " + row.tagName()); + } + addRowAt(getRowCount()).readDesign(row, designContext); + } + } + + /** * Returns an unmodifiable list of the rows in this section. * * @return the rows in this section @@ -448,4 +666,5 @@ public abstract class StaticSection<ROW extends StaticSection.StaticRow<?>> protected List<ROW> getRows() { return Collections.unmodifiableList(rows); } + } diff --git a/server/src/test/java/com/vaadin/tests/design/DeclarativeTestBaseBase.java b/server/src/test/java/com/vaadin/tests/design/DeclarativeTestBaseBase.java index 1bab760530..041df2deee 100644 --- a/server/src/test/java/com/vaadin/tests/design/DeclarativeTestBaseBase.java +++ b/server/src/test/java/com/vaadin/tests/design/DeclarativeTestBaseBase.java @@ -221,9 +221,9 @@ public abstract class DeclarativeTestBaseBase<T extends Component> { return context; } - public void testWrite(String design, T expected) { + public void testWrite(String expected, T component) { TestLogHandler l = new TestLogHandler(); - testWrite(design, expected, false); + testWrite(expected, component, false); Assert.assertEquals("", l.getMessages()); } diff --git a/server/src/test/java/com/vaadin/tests/server/component/abstractcomponent/AbstractComponentDeclarativeTestBase.java b/server/src/test/java/com/vaadin/tests/server/component/abstractcomponent/AbstractComponentDeclarativeTestBase.java index 7733855e95..ea7e9c77a3 100644 --- a/server/src/test/java/com/vaadin/tests/server/component/abstractcomponent/AbstractComponentDeclarativeTestBase.java +++ b/server/src/test/java/com/vaadin/tests/server/component/abstractcomponent/AbstractComponentDeclarativeTestBase.java @@ -92,16 +92,24 @@ public abstract class AbstractComponentDeclarativeTestBase<T extends AbstractCom boolean visible = false; boolean requiredIndicator = true; + T component = getComponentClass().newInstance(); + + boolean hasReadOnly = callBooleanSetter(readOnly, "setReadOnly", + component); + boolean hasRequiredIndicator = callBooleanSetter(requiredIndicator, + "setRequiredIndicatorVisible", component); + String design = String.format( "<%s id='%s' caption='%s' caption-as-html description='%s' " + "error='%s' enabled='false' width='%s' height='%s' " + "icon='%s' locale='%s' primary-style-name='%s' " - + "readonly responsive style-name='%s' visible='false' " - + "required-indicator-visible/>", + + "%s responsive style-name='%s' visible='false' " + + "%s/>", getComponentTag(), id, caption, description, error, width, - height, icon, locale.toString(), primaryStyle, styleName); + height, icon, locale.toString(), primaryStyle, + hasReadOnly ? "readonly" : "", styleName, + hasRequiredIndicator ? "required-indicator-visible" : ""); - T component = getComponentClass().newInstance(); component.setId(id); component.setCaption(caption); component.setCaptionAsHtml(captionAsHtml); @@ -115,9 +123,6 @@ public abstract class AbstractComponentDeclarativeTestBase<T extends AbstractCom component.setIcon(new FileResource(new File(icon))); component.setLocale(locale); component.setPrimaryStyleName(primaryStyle); - callBooleanSetter(readOnly, "setReadOnly", component); - callBooleanSetter(requiredIndicator, "setRequiredIndicatorVisible", - component); component.setResponsive(responsive); component.setStyleName(styleName); component.setVisible(visible); @@ -126,15 +131,17 @@ public abstract class AbstractComponentDeclarativeTestBase<T extends AbstractCom testWrite(design, component); } - private void callBooleanSetter(boolean value, String setterName, + private boolean callBooleanSetter(boolean value, String setterName, T component) throws IllegalAccessException, InvocationTargetException { try { Method method = component.getClass().getMethod(setterName, new Class[] { boolean.class }); method.invoke(component, value); + return true; } catch (NoSuchMethodException ignore) { // ignore if there is no such method + return false; } } diff --git a/server/src/test/java/com/vaadin/tests/server/component/grid/GridDeclarativeTest.java b/server/src/test/java/com/vaadin/tests/server/component/grid/GridDeclarativeTest.java new file mode 100644 index 0000000000..9cdab40e01 --- /dev/null +++ b/server/src/test/java/com/vaadin/tests/server/component/grid/GridDeclarativeTest.java @@ -0,0 +1,731 @@ +/* + * 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 java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.List; +import java.util.Locale; + +import org.jsoup.nodes.Element; +import org.jsoup.parser.Tag; +import org.junit.Assert; +import org.junit.Test; + +import com.vaadin.data.SelectionModel.Multi; +import com.vaadin.data.SelectionModel.Single; +import com.vaadin.server.data.DataProvider; +import com.vaadin.shared.ui.grid.HeightMode; +import com.vaadin.shared.ui.label.ContentMode; +import com.vaadin.tests.data.bean.Person; +import com.vaadin.tests.server.component.abstractlisting.AbstractListingDeclarativeTest; +import com.vaadin.ui.Grid; +import com.vaadin.ui.Grid.Column; +import com.vaadin.ui.Grid.FooterCell; +import com.vaadin.ui.Grid.FooterRow; +import com.vaadin.ui.Grid.HeaderCell; +import com.vaadin.ui.Grid.HeaderRow; +import com.vaadin.ui.Grid.SelectionMode; +import com.vaadin.ui.Label; +import com.vaadin.ui.declarative.DesignContext; +import com.vaadin.ui.declarative.DesignException; + +/** + * @author Vaadin Ltd + * + */ +public class GridDeclarativeTest extends AbstractListingDeclarativeTest<Grid> { + + @Test + public void gridAttributes() { + Grid<Person> grid = new Grid<>(); + int frozenColumns = 1; + HeightMode heightMode = HeightMode.ROW; + double heightByRows = 13.7d; + + grid.addColumn(Person::getFirstName); + grid.addColumn("id", Person::getLastName); + + grid.setFrozenColumnCount(frozenColumns); + grid.setSelectionMode(SelectionMode.MULTI); + grid.setHeightMode(heightMode); + grid.setHeightByRows(heightByRows); + + String design = String.format( + "<%s height-mode='%s' frozen-columns='%d' rows='%s' selection-mode='%s'><table><colgroup>" + + "<col column-id='generatedColumn0' sortable>" + + "<col column-id='id' sortable>" + "</colgroup><thead>" + + "<tr default><th plain-text column-ids='generatedColumn0'>Generated Column0</th>" + + "<th plain-text column-ids='id'>Id</th></tr>" + + "</thead></table></%s>", + getComponentTag(), + heightMode.toString().toLowerCase(Locale.ENGLISH), + frozenColumns, heightByRows, + SelectionMode.MULTI.toString().toLowerCase(Locale.ENGLISH), + getComponentTag()); + + testRead(design, grid); + testWrite(design, grid); + } + + @Test + public void mergedHeaderCells() { + Grid<Person> grid = new Grid<>(); + + Column<Person, String> column1 = grid.addColumn(Person::getFirstName); + Column<Person, String> column2 = grid.addColumn("id", + Person::getLastName); + Column<Person, String> column3 = grid.addColumn("mail", + Person::getEmail); + + HeaderRow header = grid.addHeaderRowAt(1); + String headerRowText1 = "foo"; + header.getCell(column1).setText(headerRowText1); + HeaderCell cell2 = header.getCell(column2); + HeaderCell join = header.join(cell2, header.getCell(column3)); + String headerRowText3 = "foobar"; + join.setText(headerRowText3); + + String design = String.format("<%s><table><colgroup>" + + "<col column-id='generatedColumn0' sortable>" + + "<col column-id='id' sortable>" + + "<col column-id='mail' sortable>" + "</colgroup><thead>" + + "<tr default><th plain-text column-ids='generatedColumn0'>Generated Column0</th>" + + "<th plain-text column-ids='id'>Id</th>" + + "<th plain-text column-ids='mail'>Mail</th></tr>" + + "<tr><th plain-text column-ids='generatedColumn0'>%s</th>" + + "<th colspan='2' plain-text column-ids='id,mail'>foobar</th></tr>" + + "</thead></table></%s>", getComponentTag(), headerRowText1, + headerRowText3, getComponentTag()); + + testRead(design, grid); + testWrite(design, grid); + } + + @Test + public void mergedFooterCells() { + Grid<Person> grid = new Grid<>(); + + Column<Person, String> column1 = grid.addColumn(Person::getFirstName); + Column<Person, String> column2 = grid.addColumn("id", + Person::getLastName); + Column<Person, String> column3 = grid.addColumn("mail", + Person::getEmail); + + FooterRow footer = grid.addFooterRowAt(0); + + FooterCell cell1 = footer.getCell(column1); + String footerRowText1 = "foo"; + cell1.setText(footerRowText1); + + FooterCell cell2 = footer.getCell(column2); + + FooterCell cell3 = footer.getCell(column3); + String footerRowText2 = "foobar"; + footer.join(cell2, cell3).setHtml(footerRowText2); + + String design = String.format("<%s><table><colgroup>" + + "<col column-id='generatedColumn0' sortable>" + + "<col column-id='id' sortable>" + + "<col column-id='mail' sortable>" + "</colgroup><thead>" + + "<tr default><th plain-text column-ids='generatedColumn0'>Generated Column0</th>" + + "<th plain-text column-ids='id'>Id</th>" + + "<th plain-text column-ids='mail'>Mail</th></tr></thead>" + + "<tfoot><tr><td plain-text column-ids='generatedColumn0'>%s</td>" + + "<td colspan='2' column-ids='id,mail'>%s</td></tr></tfoot>" + + "</table></%s>", getComponentTag(), footerRowText1, + footerRowText2, getComponentTag()); + + testRead(design, grid); + testWrite(design, grid); + } + + @Test + public void columnAttributes() { + Grid<Person> grid = new Grid<>(); + + String secondColumnId = "id"; + Column<Person, String> column1 = grid.addColumn(Person::getFirstName); + Column<Person, String> column2 = grid.addColumn(secondColumnId, + Person::getLastName); + + String caption = "test-caption"; + column1.setCaption(caption); + boolean sortable = false; + column1.setSortable(sortable); + boolean editable = true; + column1.setEditorComponentGenerator(component -> null); + column1.setEditable(editable); + boolean resizable = false; + column1.setResizable(resizable); + boolean hidable = true; + column1.setHidable(hidable); + boolean hidden = true; + column1.setHidden(hidden); + + String hidingToggleCaption = "toggle-caption"; + column2.setHidingToggleCaption(hidingToggleCaption); + double width = 17.3; + column2.setWidth(width); + double minWidth = 37.3; + column2.setMinimumWidth(minWidth); + double maxWidth = 63.4; + column2.setMaximumWidth(maxWidth); + int expandRatio = 83; + column2.setExpandRatio(expandRatio); + + String design = String.format( + "<%s><table><colgroup>" + + "<col column-id='generatedColumn0' sortable='%s' editable resizable='%s' hidable hidden>" + + "<col column-id='id' sortable hiding-toggle-caption='%s' width='%s' min-width='%s' max-width='%s' expand='%s'>" + + "</colgroup><thead>" + + "<tr default><th plain-text column-ids='generatedColumn0'>%s</th>" + + "<th plain-text column-ids='id'>%s</th>" + + "</tr></thead>" + "</table></%s>", + getComponentTag(), sortable, resizable, hidingToggleCaption, + width, minWidth, maxWidth, expandRatio, caption, "Id", + getComponentTag()); + + testRead(design, grid, true); + testWrite(design, grid); + } + + @Test + public void headerFooterSerialization() { + Grid<Person> grid = new Grid<>(); + + Column<Person, String> column1 = grid.addColumn(Person::getFirstName); + Column<Person, String> column2 = grid.addColumn("id", + Person::getLastName); + + FooterRow footerRow = grid.addFooterRowAt(0); + footerRow.getCell(column1).setText("x"); + footerRow.getCell(column2).setHtml("y"); + + String design = String.format("<%s><table><colgroup>" + + "<col column-id='generatedColumn0' sortable>" + + "<col column-id='id' sortable></colgroup><thead>" + + "<tr default><th plain-text column-ids='generatedColumn0'>Generated Column0</th>" + + "<th plain-text column-ids='id'>Id</th></tr>" + + "</thead><tbody></tbody>" + + "<tfoot><tr><td plain-text column-ids='generatedColumn0'>x</td>" + + "<td column-ids='id'>y</td></tr></tfoot>" + "</table></%s>", + getComponentTag(), getComponentTag()); + + testRead(design, grid); + testWrite(design, grid, true); + } + + @Override + public void dataSerialization() throws InstantiationException, + IllegalAccessException, InvocationTargetException { + Grid<Person> grid = new Grid<>(); + + Person person1 = createPerson("foo", "bar"); + Person person2 = createPerson("name", "last-name"); + grid.setItems(person1, person2); + + grid.addColumn(Person::getFirstName); + grid.addColumn("id", Person::getLastName); + + String design = String.format( + "<%s><table><colgroup>" + + "<col column-id='generatedColumn0' sortable>" + + "<col column-id='id' sortable></colgroup><thead>" + + "<tr default><th plain-text column-ids='generatedColumn0'>Generated Column0</th>" + + "<th plain-text column-ids='id'>Id</th></tr>" + + "</thead><tbody>" + + "<tr item='%s'><td>%s</td><td>%s</td></tr>" + + "<tr item='%s'><td>%s</td><td>%s</td></tr>" + + "</tbody></table></%s>", + getComponentTag(), person1.toString(), person1.getFirstName(), + person1.getLastName(), person2.toString(), + person2.getFirstName(), person2.getLastName(), + getComponentTag()); + + testRead(design, grid); + testWrite(design, grid, true); + } + + /** + * Value for single select + */ + @Override + @Test + public void valueSerialization() throws InstantiationException, + IllegalAccessException, InvocationTargetException { + valueSingleSelectSerialization(); + } + + @SuppressWarnings("unchecked") + @Test + public void valueMultiSelectSerialization() throws InstantiationException, + IllegalAccessException, InvocationTargetException { + Grid<Person> grid = new Grid<>(); + + Person person1 = createPerson("foo", "bar"); + Person person2 = createPerson("name", "last-name"); + Person person3 = createPerson("foo", "last-name"); + grid.setItems(person1, person2, person3); + + grid.addColumn(Person::getFirstName); + grid.addColumn("id", Person::getLastName); + + Multi<Person> model = (Multi<Person>) grid + .setSelectionMode(SelectionMode.MULTI); + model.selectItems(person1, person3); + + String design = String.format( + "<%s selection-mode='multi'><table><colgroup>" + + "<col column-id='generatedColumn0' sortable>" + + "<col column-id='id' sortable></colgroup><thead>" + + "<tr default><th plain-text column-ids='generatedColumn0'>Generated Column0</th>" + + "<th plain-text column-ids='id'>Id</th></tr>" + + "</thead><tbody>" + + "<tr item='%s' selected><td>%s</td><td>%s</td></tr>" + + "<tr item='%s'><td>%s</td><td>%s</td></tr>" + + "<tr item='%s' selected><td>%s</td><td>%s</td></tr>" + + "</tbody></table></%s>", + getComponentTag(), person1.toString(), person1.getFirstName(), + person1.getLastName(), person2.toString(), + person2.getFirstName(), person2.getLastName(), + person3.toString(), person3.getFirstName(), + person3.getLastName(), getComponentTag()); + + testRead(design, grid); + testWrite(design, grid, true); + } + + @SuppressWarnings("unchecked") + private void valueSingleSelectSerialization() throws InstantiationException, + IllegalAccessException, InvocationTargetException { + Grid<Person> grid = new Grid<>(); + + Person person1 = createPerson("foo", "bar"); + Person person2 = createPerson("name", "last-name"); + grid.setItems(person1, person2); + + grid.addColumn(Person::getFirstName); + grid.addColumn("id", Person::getLastName); + + Single<Person> model = (Single<Person>) grid + .setSelectionMode(SelectionMode.SINGLE); + model.select(person2); + + String design = String.format( + "<%s><table><colgroup>" + + "<col column-id='generatedColumn0' sortable>" + + "<col column-id='id' sortable></colgroup><thead>" + + "<tr default><th plain-text column-ids='generatedColumn0'>Generated Column0</th>" + + "<th plain-text column-ids='id'>Id</th></tr>" + + "</thead><tbody>" + + "<tr item='%s'><td>%s</td><td>%s</td></tr>" + + "<tr item='%s' selected><td>%s</td><td>%s</td></tr>" + + "</tbody></table></%s>", + getComponentTag(), person1.toString(), person1.getFirstName(), + person1.getLastName(), person2.toString(), + person2.getFirstName(), person2.getLastName(), + getComponentTag()); + + testRead(design, grid); + testWrite(design, grid, true); + } + + @Override + public void readOnlySelection() throws InstantiationException, + IllegalAccessException, InvocationTargetException { + Grid<Person> grid = new Grid<>(); + + Person person1 = createPerson("foo", "bar"); + Person person2 = createPerson("name", "last-name"); + grid.setItems(person1, person2); + + grid.addColumn(Person::getFirstName); + grid.addColumn("id", Person::getLastName); + + grid.setSelectionMode(SelectionMode.MULTI); + grid.asMultiSelect().setReadOnly(true); + + String formatString = "<%s %s selection-allowed><table><colgroup>" + + "<col column-id='generatedColumn0' sortable>" + + "<col column-id='id' sortable>" + "</colgroup><thead>" + + "<tr default><th plain-text column-ids='generatedColumn0'>Generated Column0</th>" + + "<th plain-text column-ids='id'>Id</th></tr>" + + "</thead><tbody>" + + "<tr item='%s'><td>%s</td><td>%s</td></tr>" + + "<tr item='%s'><td>%s</td><td>%s</td></tr>" + + "</tbody></table></%s>"; + + String design = String.format(formatString, getComponentTag(), + "selection-mode='multi'", person1.toString(), + person1.getFirstName(), person1.getLastName(), + person2.toString(), person2.getFirstName(), + person2.getLastName(), getComponentTag()); + + testRead(design, grid); + testWrite(design, grid, true); + + grid.setSelectionMode(SelectionMode.SINGLE); + grid.asSingleSelect().setReadOnly(true); + + design = String.format(formatString, getComponentTag(), "", + person1.toString(), person1.getFirstName(), + person1.getLastName(), person2.toString(), + person2.getFirstName(), person2.getLastName(), + getComponentTag()); + + testRead(design, grid); + testWrite(design, grid, true); + } + + @Test + public void testComponentInGridHeader() { + Grid<Person> grid = new Grid<>(); + Column<Person, String> column = grid.addColumn(Person::getFirstName); + + String html = "<b>Foo</b>"; + Label component = new Label(html); + component.setContentMode(ContentMode.HTML); + + //@formatter:off + String design = String.format( "<%s><table>" + + "<colgroup>" + + " <col sortable column-id='generatedColumn0'>" + + "</colgroup>" + + "<thead>" + + "<tr default><th column-ids='generatedColumn0'><vaadin-label>%s</vaadin-label></th></tr>" + + "</thead>" + + "</table></%s>", getComponentTag(), html, getComponentTag()); + //@formatter:on + + grid.getDefaultHeaderRow().getCell(column.getId()) + .setComponent(component); + + testRead(design, grid, true); + testWrite(design, grid); + } + + @Test + public void testComponentInGridFooter() { + Grid<Person> grid = new Grid<>(); + Column<Person, String> column = grid.addColumn(Person::getFirstName); + + String html = "<b>Foo</b>"; + Label component = new Label(html); + component.setContentMode(ContentMode.HTML); + + grid.prependFooterRow().getCell(column).setComponent(component); + grid.removeHeaderRow(grid.getDefaultHeaderRow()); + + //@formatter:off + String design = String.format( "<%s><table>" + + "<colgroup>" + + " <col sortable column-id='generatedColumn0'>" + + "</colgroup>" + + "<thead>" + +"<tfoot>" + + "<tr><td column-ids='generatedColumn0'><vaadin-label>%s</vaadin-label></td></tr>" + + "</tfoot>" + + "</table>" + + "</%s>", getComponentTag(), html, getComponentTag()); + //@formatter:on + + testRead(design, grid, true); + testWrite(design, grid); + } + + @Test + public void testNoHeaderRows() { + //@formatter:off + String design = "<vaadin-grid><table>" + + "<colgroup>" + + " <col sortable column-id='generatedColumn0'>" + + "</colgroup>" + + "<thead />" + + "</table>" + + "</vaadin-grid>"; + //@formatter:on + Grid<Person> grid = new Grid<>(); + grid.addColumn(Person::getFirstName); + grid.removeHeaderRow(grid.getDefaultHeaderRow()); + + testWrite(design, grid); + testRead(design, grid, true); + } + + @Test + public void testReadEmptyGrid() { + String design = "<vaadin-grid />"; + testRead(design, new Grid<String>(), false); + } + + @Test + public void testEmptyGrid() { + String design = "<vaadin-grid></vaadin-grid>"; + Grid<String> expected = new Grid<>(); + testWrite(design, expected); + testRead(design, expected, true); + } + + @Test(expected = DesignException.class) + public void testMalformedGrid() { + String design = "<vaadin-grid><vaadin-label /></vaadin-grid>"; + testRead(design, new Grid<String>()); + } + + @Test(expected = DesignException.class) + public void testGridWithNoColGroup() { + String design = "<vaadin-grid><table><thead><tr><th>Foo</tr></thead></table></vaadin-grid>"; + testRead(design, new Grid<String>()); + } + + @Test + @SuppressWarnings("unchecked") + public void testHtmlEntitiesinGridHeaderFooter() { + String id = "> id"; + String plainText = "plain-text"; + //@formatter:off + String design = String.format( "<%s><table>" + + "<colgroup>" + + " <col sortable column-id='%s'>" + + "</colgroup>" + + "<thead>" + +" <tr default><th %s column-ids='%s'>> Test</th>" + + "</thead>" + + "<tfoot>" + + "<tr><td %s column-ids='%s'>> Test</td></tr>" + + "</tfoot>" + + "<tbody />" + + "</table></%s>", + getComponentTag() , id, plainText, id, plainText, id, getComponentTag()); + //@formatter:on + + Grid<Person> grid = read(design); + String actualHeader = grid.getHeaderRow(0).getCell(id).getText(); + String actualFooter = grid.getFooterRow(0).getCell(id).getText(); + String expected = "> Test"; + + Assert.assertEquals(expected, actualHeader); + Assert.assertEquals(expected, actualFooter); + + design = design.replace(plainText, ""); + grid = read(design); + actualHeader = grid.getHeaderRow(0).getCell(id).getHtml(); + actualFooter = grid.getFooterRow(0).getCell(id).getHtml(); + expected = "> Test"; + + Assert.assertEquals(expected, actualHeader); + Assert.assertEquals(expected, actualFooter); + + grid = new Grid<>(); + Column<Person, String> column = grid.addColumn(id, + Person::getFirstName); + HeaderRow header = grid.addHeaderRowAt(0); + FooterRow footer = grid.addFooterRowAt(0); + grid.removeHeaderRow(grid.getDefaultHeaderRow()); + + // entities should be encoded when writing back, not interpreted as HTML + header.getCell(column).setText("& Test"); + footer.getCell(column).setText("& Test"); + + Element root = new Element(Tag.valueOf(getComponentTag()), ""); + grid.writeDesign(root, new DesignContext()); + + Assert.assertEquals("&amp; Test", + root.getElementsByTag("th").get(0).html()); + Assert.assertEquals("&amp; Test", + root.getElementsByTag("td").get(0).html()); + + header = grid.addHeaderRowAt(0); + footer = grid.addFooterRowAt(0); + + // entities should not be encoded, this is already given as HTML + header.getCell(id).setHtml("& Test"); + footer.getCell(id).setHtml("& Test"); + + root = new Element(Tag.valueOf(getComponentTag()), ""); + grid.writeDesign(root, new DesignContext()); + + Assert.assertEquals("& Test", + root.getElementsByTag("th").get(0).html()); + Assert.assertEquals("& Test", + root.getElementsByTag("td").get(0).html()); + + } + + @SuppressWarnings("rawtypes") + @Override + public Grid<?> testRead(String design, Grid expected) { + return testRead(design, expected, false); + } + + @SuppressWarnings("rawtypes") + public Grid<?> testRead(String design, Grid expected, boolean retestWrite) { + return testRead(design, expected, retestWrite, false); + } + + @SuppressWarnings("rawtypes") + public Grid<?> testRead(String design, Grid expected, boolean retestWrite, + boolean writeData) { + Grid<?> actual = super.testRead(design, expected); + + compareGridColumns(expected, actual); + compareHeaders(expected, actual); + compareFooters(expected, actual); + + if (retestWrite) { + testWrite(design, actual, writeData); + } + + return actual; + } + + private void compareHeaders(Grid<?> expected, Grid<?> actual) { + Assert.assertEquals("Different header row count", + expected.getHeaderRowCount(), actual.getHeaderRowCount()); + for (int i = 0; i < expected.getHeaderRowCount(); ++i) { + HeaderRow expectedRow = expected.getHeaderRow(i); + HeaderRow actualRow = actual.getHeaderRow(i); + + if (expectedRow.equals(expected.getDefaultHeaderRow())) { + Assert.assertEquals("Different index for default header row", + actual.getDefaultHeaderRow(), actualRow); + } + + for (Column<?, ?> column : expected.getColumns()) { + String baseError = "Difference when comparing cell for " + + column.toString() + " on header row " + i + ": "; + HeaderCell expectedCell = expectedRow.getCell(column); + HeaderCell actualCell = actualRow.getCell(column); + + switch (expectedCell.getCellType()) { + case TEXT: + Assert.assertEquals(baseError + "Text content", + expectedCell.getText(), actualCell.getText()); + break; + case HTML: + Assert.assertEquals(baseError + "HTML content", + expectedCell.getHtml(), actualCell.getHtml()); + break; + case WIDGET: + assertEquals(baseError + "Component content", + expectedCell.getComponent(), + actualCell.getComponent()); + break; + } + } + } + } + + private void compareFooters(Grid<?> expected, Grid<?> actual) { + Assert.assertEquals("Different footer row count", + expected.getFooterRowCount(), actual.getFooterRowCount()); + for (int i = 0; i < expected.getFooterRowCount(); ++i) { + FooterRow expectedRow = expected.getFooterRow(i); + FooterRow actualRow = actual.getFooterRow(i); + + for (Column<?, ?> column : expected.getColumns()) { + String baseError = "Difference when comparing cell for " + + column.toString() + " on footer row " + i + ": "; + FooterCell expectedCell = expectedRow.getCell(column); + FooterCell actualCell = actualRow.getCell(column); + + switch (expectedCell.getCellType()) { + case TEXT: + Assert.assertEquals(baseError + "Text content", + expectedCell.getText(), actualCell.getText()); + break; + case HTML: + Assert.assertEquals(baseError + "HTML content", + expectedCell.getHtml(), actualCell.getHtml()); + break; + case WIDGET: + assertEquals(baseError + "Component content", + expectedCell.getComponent(), + actualCell.getComponent()); + break; + } + } + } + } + + private void compareGridColumns(Grid<?> expected, Grid<?> actual) { + List<?> columns = expected.getColumns(); + List<?> actualColumns = actual.getColumns(); + Assert.assertEquals("Different amount of columns", columns.size(), + actualColumns.size()); + for (int i = 0; i < columns.size(); ++i) { + Column<?, ?> col1 = (Column<?, ?>) columns.get(i); + Column<?, ?> col2 = (Column<?, ?>) actualColumns.get(i); + String baseError = "Error when comparing columns for property " + + col1.getId() + ": "; + assertEquals(baseError + "Width", col1.getWidth(), col2.getWidth()); + assertEquals(baseError + "Maximum width", col1.getMaximumWidth(), + col2.getMaximumWidth()); + assertEquals(baseError + "Minimum width", col1.getMinimumWidth(), + col2.getMinimumWidth()); + assertEquals(baseError + "Expand ratio", col1.getExpandRatio(), + col2.getExpandRatio()); + assertEquals(baseError + "Sortable", col1.isSortable(), + col2.isSortable()); + assertEquals(baseError + "Editable", col1.isEditable(), + col2.isEditable()); + assertEquals(baseError + "Hidable", col1.isHidable(), + col2.isHidable()); + assertEquals(baseError + "Hidden", col1.isHidden(), + col2.isHidden()); + assertEquals(baseError + "HidingToggleCaption", + col1.getHidingToggleCaption(), + col2.getHidingToggleCaption()); + } + } + + @Override + protected String getComponentTag() { + return "vaadin-grid"; + } + + @Override + protected Class<? extends Grid> getComponentClass() { + return Grid.class; + } + + @Override + protected boolean acceptProperty(Class<?> clazz, Method readMethod, + Method writeMethod) { + if (readMethod != null) { + Class<?> returnType = readMethod.getReturnType(); + if (HeaderRow.class.equals(returnType) + || DataProvider.class.equals(returnType)) { + return false; + } + } + return super.acceptProperty(clazz, readMethod, writeMethod); + } + + private Person createPerson(String name, String lastName) { + Person person = new Person() { + @Override + public String toString() { + return getFirstName() + " " + getLastName(); + } + }; + person.setFirstName(name); + person.setLastName(lastName); + return person; + } + +} 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 4f9d733f44..c9c5614205 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 @@ -66,7 +66,7 @@ public class GridBasics extends AbstractTestUIWithLog { public static final String CELL_STYLE_GENERATOR_EMPTY = "Empty string"; public static final String CELL_STYLE_GENERATOR_NULL = "Null"; - private boolean isUserSelectionAllowed = true; + private boolean isUserSelectionDisallowed; public static final String[] COLUMN_CAPTIONS = { "Column 0", "Column 1", "Column 2", "Row Number", "Date", "HTML String", "Big Random", @@ -408,20 +408,20 @@ public class GridBasics extends AbstractTestUIWithLog { }).setCheckable(true); MenuItem selectionAllowedItem = stateMenu - .addItem("Allow user selection", item -> { - isUserSelectionAllowed = !isUserSelectionAllowed; + .addItem("Disallow user selection", item -> { + isUserSelectionDisallowed = !isUserSelectionDisallowed; if (grid.getSelectionModel() instanceof MultiSelectionModelImpl) { MultiSelect<DataObject> multiSelect = grid .asMultiSelect(); - multiSelect.setReadOnly(isUserSelectionAllowed); + multiSelect.setReadOnly(isUserSelectionDisallowed); } if (grid.getSelectionModel() instanceof SingleSelectionModelImpl) { SingleSelect<DataObject> singleSelect = grid .asSingleSelect(); - singleSelect.setReadOnly(isUserSelectionAllowed); + singleSelect.setReadOnly(isUserSelectionDisallowed); } }); - selectionAllowedItem.setChecked(true); + selectionAllowedItem.setChecked(false); selectionAllowedItem.setCheckable(true); stateMenu.addItem("Column reorder listener", @@ -513,7 +513,7 @@ public class GridBasics extends AbstractTestUIWithLog { selectionListenerRegistration = ((SingleSelectionModelImpl<DataObject>) grid .getSelectionModel()) .addSingleSelectionListener(this::onSingleSelect); - grid.asSingleSelect().setReadOnly(isUserSelectionAllowed); + grid.asSingleSelect().setReadOnly(isUserSelectionDisallowed); }); selectionModelItem.addItem("multi", menuItem -> { switchToMultiSelect(); @@ -559,7 +559,7 @@ public class GridBasics extends AbstractTestUIWithLog { MultiSelectionModelImpl<DataObject> model = (MultiSelectionModelImpl<DataObject>) grid .setSelectionMode(SelectionMode.MULTI); model.addMultiSelectionListener(this::onMultiSelect); - grid.asMultiSelect().setReadOnly(isUserSelectionAllowed); + grid.asMultiSelect().setReadOnly(isUserSelectionDisallowed); selectionListenerRegistration = model .addMultiSelectionListener(this::onMultiSelect); } diff --git a/uitest/src/test/java/com/vaadin/tests/components/grid/GridSelectionTest.java b/uitest/src/test/java/com/vaadin/tests/components/grid/GridSelectionTest.java index 699edf462b..97dbf07d1f 100644 --- a/uitest/src/test/java/com/vaadin/tests/components/grid/GridSelectionTest.java +++ b/uitest/src/test/java/com/vaadin/tests/components/grid/GridSelectionTest.java @@ -404,7 +404,7 @@ public class GridSelectionTest extends GridBasicsTest { } private void toggleUserSelectionAllowed() { - selectMenuPath("Component", "State", "Allow user selection"); + selectMenuPath("Component", "State", "Disallow user selection"); } private WebElement getSelectionCheckbox(int row) { |