From d37b2d430eca6b0eeacb48626c0bbfb33d1502de Mon Sep 17 00:00:00 2001 From: Teemu Suo-Anttila Date: Thu, 17 Nov 2016 17:05:28 +0200 Subject: Reintroduce Grid Editor using Binder This patch restores the bean type to BinderValidationStatusHandler Change-Id: I9ace77a492c4823c15591fb1426e9bd216895fb0 --- server/src/main/java/com/vaadin/data/Binder.java | 8 +- .../vaadin/data/BinderValidationStatusHandler.java | 11 +- server/src/main/java/com/vaadin/ui/Grid.java | 275 +++++++++++++++++- .../com/vaadin/ui/components/grid/EditorImpl.java | 310 +++++++++++++++++++++ .../com/vaadin/data/BinderBookOfVaadinTest.java | 2 +- 5 files changed, 595 insertions(+), 11 deletions(-) create mode 100644 server/src/main/java/com/vaadin/ui/components/grid/EditorImpl.java (limited to 'server') diff --git a/server/src/main/java/com/vaadin/data/Binder.java b/server/src/main/java/com/vaadin/data/Binder.java index 771726ea3c..df6d72d79c 100644 --- a/server/src/main/java/com/vaadin/data/Binder.java +++ b/server/src/main/java/com/vaadin/data/Binder.java @@ -884,7 +884,7 @@ public class Binder implements Serializable { private Label statusLabel; - private BinderValidationStatusHandler statusHandler; + private BinderValidationStatusHandler statusHandler; private boolean hasChanges = false; @@ -1356,7 +1356,7 @@ public class Binder implements Serializable { * @see Binding#withValidationStatusHandler(ValidationStatusHandler) */ public void setValidationStatusHandler( - BinderValidationStatusHandler statusHandler) { + BinderValidationStatusHandler statusHandler) { Objects.requireNonNull(statusHandler, "Cannot set a null " + BinderValidationStatusHandler.class.getSimpleName()); if (statusLabel != null) { @@ -1377,7 +1377,7 @@ public class Binder implements Serializable { * @return the status handler used, never null * @see #setValidationStatusHandler(BinderStatusHandler) */ - public BinderValidationStatusHandler getValidationStatusHandler() { + public BinderValidationStatusHandler getValidationStatusHandler() { return Optional.ofNullable(statusHandler) .orElse(this::handleBinderValidationStatus); } @@ -1509,7 +1509,7 @@ public class Binder implements Serializable { * validators */ protected void handleBinderValidationStatus( - BinderValidationStatus binderStatus) { + BinderValidationStatus binderStatus) { // let field events go to binding status handlers binderStatus.getFieldValidationStatuses() .forEach(status -> ((BindingImpl) status.getBinding()) diff --git a/server/src/main/java/com/vaadin/data/BinderValidationStatusHandler.java b/server/src/main/java/com/vaadin/data/BinderValidationStatusHandler.java index 691b7bd6c0..1f3a95688b 100644 --- a/server/src/main/java/com/vaadin/data/BinderValidationStatusHandler.java +++ b/server/src/main/java/com/vaadin/data/BinderValidationStatusHandler.java @@ -23,8 +23,8 @@ import com.vaadin.ui.AbstractComponent; /** * Handler for {@link BinderValidationStatus} changes. *

- * {{@link Binder#setValidationStatusHandler(BinderStatusHandler) Register} an instance of - * this class to be able to customize validation status handling. + * {{@link Binder#setValidationStatusHandler(BinderStatusHandler) Register} an + * instance of this class to be able to customize validation status handling. *

* The default handler will show * {@link AbstractComponent#setComponentError(com.vaadin.server.ErrorMessage) an @@ -39,9 +39,12 @@ import com.vaadin.ui.AbstractComponent; * @see Binder#validate() * @see ValidationStatus * + * @param + * the bean type of binder + * * @since 8.0 */ -public interface BinderValidationStatusHandler - extends Consumer>, Serializable { +public interface BinderValidationStatusHandler + extends Consumer>, Serializable { } diff --git a/server/src/main/java/com/vaadin/ui/Grid.java b/server/src/main/java/com/vaadin/ui/Grid.java index ee1547bfce..53e22a53d4 100644 --- a/server/src/main/java/com/vaadin/ui/Grid.java +++ b/server/src/main/java/com/vaadin/ui/Grid.java @@ -31,6 +31,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.function.BiFunction; import java.util.function.BinaryOperator; import java.util.function.Function; import java.util.stream.Collectors; @@ -39,6 +40,7 @@ import java.util.stream.Stream; import org.jsoup.nodes.Element; import com.vaadin.data.Binder; +import com.vaadin.data.BinderValidationStatus; import com.vaadin.data.SelectionModel; import com.vaadin.event.ConnectorEvent; import com.vaadin.event.ContextClickEvent; @@ -48,6 +50,7 @@ import com.vaadin.server.Extension; import com.vaadin.server.JsonCodec; import com.vaadin.server.SerializableComparator; import com.vaadin.server.SerializableFunction; +import com.vaadin.server.data.DataCommunicator; import com.vaadin.server.data.SortOrder; import com.vaadin.shared.MouseEventDetails; import com.vaadin.shared.Registration; @@ -63,6 +66,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.components.grid.EditorImpl; import com.vaadin.ui.components.grid.Footer; import com.vaadin.ui.components.grid.Header; import com.vaadin.ui.components.grid.Header.Row; @@ -587,6 +591,11 @@ public class Grid extends AbstractListing implements HasComponents { .getSortOrder(order.getDirection())) .forEach(s -> s.forEach(sortProperties::add)); getDataCommunicator().setBackEndSorting(sortProperties); + + // Close grid editor if it's open. + if (getEditor().isOpen()) { + getEditor().cancel(); + } } @Override @@ -795,6 +804,8 @@ public class Grid extends AbstractListing implements HasComponents { private StyleGenerator styleGenerator = item -> null; private DescriptionGenerator descriptionGenerator; + private SerializableFunction componentGenerator; + /** * Constructs a new Column configuration with given header caption, * renderer and value provider. @@ -1497,9 +1508,86 @@ public class Grid extends AbstractListing implements HasComponents { return getState(false).resizable; } + /** + * Sets whether this Column has a component displayed in Editor or not. + * + * @param editable + * {@code true} if column is editable; {@code false} if not + * @return this column + * + * @see #setEditorComponent(Component) + * @see #setEditorComponentGenerator(SerializableFunction) + */ + public Column setEditable(boolean editable) { + Objects.requireNonNull(componentGenerator, + "Column has no editor component defined"); + getState().editable = editable; + return this; + } + + /** + * Gets whether this Column has a component displayed in Editor or not. + * + * @return {@code true} if the column displays an editor component; + * {@code false} if not + */ + public boolean isEditable() { + return getState(false).editable; + } + + /** + * Sets a static editor component for this column. + *

+ * Note: The same component cannot be used for multiple + * columns. + * + * @param component + * the editor component + * @return this column + * + * @see Editor#getBinder() + * @see Editor#setBinder(Binder) + * @see #setEditorComponentGenerator(SerializableFunction) + */ + public Column setEditorComponent(Component component) { + Objects.requireNonNull(component, + "null is not a valid editor field"); + return setEditorComponentGenerator(t -> component); + } + + /** + * Sets a component generator to provide editor component for this + * Column. This method can be used to generate any dynamic component to + * be displayed in the editor row. + *

+ * Note: The same component cannot be used for multiple + * columns. + * + * @param componentGenerator + * the editor component generator + * @return this column + * + * @see #setEditorComponent(Component) + */ + public Column setEditorComponentGenerator( + SerializableFunction componentGenerator) { + Objects.requireNonNull(componentGenerator); + this.componentGenerator = componentGenerator; + return setEditable(true); + } + + /** + * Gets the editor component generator for this Column. + * + * @return editor component generator + */ + public SerializableFunction getEditorComponentGenerator() { + return componentGenerator; + } + /** * Checks if column is attached and throws an - * {@link IllegalStateException} if it is not + * {@link IllegalStateException} if it is not. * * @throws IllegalStateException * if the column is no longer attached to any grid @@ -1557,7 +1645,7 @@ public class Grid extends AbstractListing implements HasComponents { * The cells to merge. Must be from the same row. * @return The remaining visible cell after the merge */ - HeaderCell join(HeaderCell ... cellsToMerge); + HeaderCell join(HeaderCell... cellsToMerge); } @@ -1716,6 +1804,167 @@ public class Grid extends AbstractListing implements HasComponents { public GridStaticCellType getCellType(); } + /** + * Generator for creating editor validation and conversion error messages. + * + * @param + * the bean type + */ + public interface EditorErrorGenerator extends Serializable, + BiFunction>, BinderValidationStatus, String> { + + /** + * Generates an error message from given validation status object. + * + * @param fieldToColumn + * the map of failed fields and corresponding columns + * @param status + * the binder status object with all failures + * + * @return error message string + */ + @Override + public String apply(Map> fieldToColumn, + BinderValidationStatus status); + } + + /** + * An editor in a Grid. + * + * @param + */ + public interface Editor extends Serializable { + + /** + * Sets the underlying Binder to this Editor. + * + * @param binder + * the binder for updating editor fields; not {@code null} + * @return this editor + */ + public Editor setBinder(Binder binder); + + /** + * Returns the underlying Binder from Editor. + * + * @return the binder; not {@code null} + */ + public Binder getBinder(); + + /** + * Sets the Editor buffered mode. When the editor is in buffered mode, + * edits are only committed when the user clicks the save button. In + * unbuffered mode valid changes are automatically committed. + * + * @param buffered + * {@code true} if editor should be buffered; {@code false} + * if not + * @return this editor + */ + public Editor setBuffered(boolean buffered); + + /** + * Enables or disabled the Editor. A disabled editor cannot be opened. + * + * @param enabled + * {@code true} if editor should be enabled; {@code false} if + * not + * @return this editor + */ + public Editor setEnabled(boolean enabled); + + /** + * Returns whether Editor is buffered or not. + * + * @see #setBuffered(boolean) + * + * @return {@code true} if editor is buffered; {@code false} if not + */ + public boolean isBuffered(); + + /** + * Returns whether Editor is enabled or not. + * + * @return {@code true} if editor is enabled; {@code false} if not + */ + public boolean isEnabled(); + + /** + * Returns whether Editor is open or not. + * + * @return {@code true} if editor is open; {@code false} if not + */ + public boolean isOpen(); + + /** + * Saves any changes from the Editor fields to the edited bean. + * + * @return {@code true} if save succeeded; {@code false} if not + */ + public boolean save(); + + /** + * Close the editor discarding any unsaved changes. + */ + public void cancel(); + + /** + * Sets the caption of the save button in buffered mode. + * + * @param saveCaption + * the save button caption + * @return this editor + */ + public Editor setSaveCaption(String saveCaption); + + /** + * Sets the caption of the cancel button in buffered mode. + * + * @param cancelCaption + * the cancel button caption + * @return this editor + */ + public Editor setCancelCaption(String cancelCaption); + + /** + * Gets the caption of the save button in buffered mode. + * + * @return the save button caption + */ + public String getSaveCaption(); + + /** + * Gets the caption of the cancel button in buffered mode. + * + * @return the cancel button caption + */ + public String getCancelCaption(); + + /** + * Sets the error message generator for this editor. + *

+ * The default message is a concatenation of column field validation + * failures and bean validation failures. + * + * @param errorGenerator + * the function to generate error messages; not {@code null} + * @return this editor + * + * @see EditorErrorGenerator + */ + public Editor setErrorGenerator( + EditorErrorGenerator errorGenerator); + + /** + * Gets the error message generator of this editor. + * + * @return the function that generates error messages; not {@code null} + * + * @see EditorErrorGenerator + */ + public EditorErrorGenerator getErrorGenerator(); + } + private class HeaderImpl extends Header { @Override @@ -1768,6 +2017,8 @@ public class Grid extends AbstractListing implements HasComponents { private GridSelectionModel selectionModel; + private Editor editor; + /** * Constructor for the {@link Grid} component. */ @@ -1782,6 +2033,11 @@ public class Grid extends AbstractListing implements HasComponents { addExtension(detailsManager); addDataGenerator(detailsManager); + editor = createEditor(); + if (editor instanceof Extension) { + addExtension((Extension) editor); + } + addDataGenerator((item, json) -> { String styleName = styleGenerator.apply(item); if (styleName != null && !styleName.isEmpty()) { @@ -2651,6 +2907,10 @@ public class Grid extends AbstractListing implements HasComponents { return ((SingleSelectionModel) model).asSingleSelect(); } + public Editor getEditor() { + return editor; + } + /** * Sets the selection model for this listing. *

@@ -2675,6 +2935,17 @@ public class Grid extends AbstractListing implements HasComponents { return (GridState) super.getState(markAsDirty); } + /** + * Creates a new Editor instance. Can be overridden to create a custom + * Editor. If the Editor is a {@link AbstractGridExtension}, it will be + * automatically added to {@link DataCommunicator}. + * + * @return editor + */ + protected Editor createEditor() { + return new EditorImpl<>(); + } + private void addExtensionComponent(Component c) { if (extensionComponents.add(c)) { c.setParent(this); diff --git a/server/src/main/java/com/vaadin/ui/components/grid/EditorImpl.java b/server/src/main/java/com/vaadin/ui/components/grid/EditorImpl.java new file mode 100644 index 0000000000..bc83547488 --- /dev/null +++ b/server/src/main/java/com/vaadin/ui/components/grid/EditorImpl.java @@ -0,0 +1,310 @@ +/* + * 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.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import com.vaadin.data.Binder; +import com.vaadin.data.BinderValidationStatus; +import com.vaadin.data.BinderValidationStatusHandler; +import com.vaadin.shared.ui.grid.editor.EditorClientRpc; +import com.vaadin.shared.ui.grid.editor.EditorServerRpc; +import com.vaadin.shared.ui.grid.editor.EditorState; +import com.vaadin.ui.Component; +import com.vaadin.ui.Grid.AbstractGridExtension; +import com.vaadin.ui.Grid.Column; +import com.vaadin.ui.Grid.Editor; +import com.vaadin.ui.Grid.EditorErrorGenerator; + +import elemental.json.JsonObject; + +/** + * Implementation of {@code Editor} interface. + * + * @param + * the grid bean type + */ +public class EditorImpl extends AbstractGridExtension + implements Editor { + + private class EditorStatusHandler + implements BinderValidationStatusHandler { + + @Override + public void accept(BinderValidationStatus status) { + boolean ok = status.isOk(); + if (saving) { + rpc.confirmSave(ok); + saving = false; + } + + if (ok) { + binder.getBean().ifPresent(t -> refresh(t)); + rpc.setErrorMessage(null, Collections.emptyList()); + } else { + List fields = status.getFieldValidationErrors() + .stream().map(error -> error.getField()) + .filter(columnFields.values()::contains) + .map(field -> (Component) field) + .collect(Collectors.toList()); + + Map> fieldToColumn = new HashMap<>(); + columnFields.entrySet().stream() + .filter(entry -> fields.contains(entry.getValue())) + .forEach(entry -> fieldToColumn.put(entry.getValue(), + entry.getKey())); + + String message = errorGenerator.apply(fieldToColumn, status); + + List columnIds = fieldToColumn.values().stream() + .map(Column::getId).collect(Collectors.toList()); + + rpc.setErrorMessage(message, columnIds); + } + } + + } + + private Binder binder; + private Map, Component> columnFields = new HashMap<>(); + private T edited; + private boolean saving = false; + private EditorClientRpc rpc; + private EditorErrorGenerator errorGenerator = (fieldToColumn, + status) -> { + String message = status.getFieldValidationErrors().stream() + .filter(e -> e.getMessage().isPresent() + && fieldToColumn.containsKey(e.getField())) + .map(e -> fieldToColumn.get(e.getField()).getCaption() + ": " + + e.getMessage().get()) + .collect(Collectors.joining("; ")); + + String beanMessage = status.getBeanValidationErrors().stream() + .map(e -> e.getErrorMessage()) + .collect(Collectors.joining("; ")); + + message = Stream.of(message, beanMessage).filter(s -> !s.isEmpty()) + .collect(Collectors.joining("; ")); + + return message; + }; + + /** + * Constructor for internal implementation of the Editor. + */ + public EditorImpl() { + rpc = getRpcProxy(EditorClientRpc.class); + registerRpc(new EditorServerRpc() { + + @Override + public void save() { + saving = true; + EditorImpl.this.save(); + } + + @Override + public void cancel() { + doClose(); + } + + @Override + public void bind(String key) { + // When in buffered mode, the editor is not allowed to move. + // Binder with failed validation returns true for hasChanges. + if (isOpen() && (isBuffered() || getBinder().hasChanges())) { + rpc.confirmBind(false); + return; + } + doClose(); + doEdit(getData(key)); + rpc.confirmBind(true); + } + }); + + setBinder(new Binder<>()); + } + + @Override + public void generateData(T item, JsonObject jsonObject) { + } + + @Override + public Editor setBinder(Binder binder) { + this.binder = binder; + + binder.setValidationStatusHandler(new EditorStatusHandler()); + return this; + } + + @Override + public Binder getBinder() { + return binder; + } + + @Override + public Editor setBuffered(boolean buffered) { + if (isOpen()) { + throw new IllegalStateException( + "Cannot modify Editor when it is open."); + } + getState().buffered = buffered; + + return this; + } + + @Override + public Editor setEnabled(boolean enabled) { + if (isOpen()) { + throw new IllegalStateException( + "Cannot modify Editor when it is open."); + } + getState().enabled = enabled; + + return this; + } + + @Override + public boolean isBuffered() { + return getState(false).buffered; + } + + @Override + public boolean isEnabled() { + return getState(false).enabled; + } + + /** + * Handles editor component generation and adding them to the hierarchy of + * the Grid. + * + * @param bean + * the edited item; can't be {@code null} + */ + protected void doEdit(T bean) { + Objects.requireNonNull(bean, "Editor can't edit null"); + if (!isEnabled()) { + throw new IllegalStateException( + "Editing is not allowed when Editor is disabled."); + } + + if (!isBuffered()) { + binder.setBean(bean); + } else { + binder.readBean(bean); + } + edited = bean; + + getParent().getColumns().stream().filter(Column::isEditable) + .forEach(c -> { + Component component = c.getEditorComponentGenerator() + .apply(edited); + addComponentToGrid(component); + columnFields.put(c, component); + getState().columnFields.put(c.getId(), + component.getConnectorId()); + }); + } + + @Override + public boolean save() { + if (isOpen() && isBuffered()) { + binder.validate(); + if (binder.writeBeanIfValid(edited)) { + refresh(edited); + return true; + } + } + return false; + } + + @Override + public boolean isOpen() { + return edited != null; + } + + @Override + public void cancel() { + doClose(); + rpc.cancel(); + } + + /** + * Handles clean up for closing the Editor. + */ + protected void doClose() { + edited = null; + + for (Component c : columnFields.values()) { + removeComponentFromGrid(c); + } + columnFields.clear(); + getState().columnFields.clear(); + } + + @Override + public Editor setSaveCaption(String saveCaption) { + Objects.requireNonNull(saveCaption); + getState().saveCaption = saveCaption; + + return this; + } + + @Override + public Editor setCancelCaption(String cancelCaption) { + Objects.requireNonNull(cancelCaption); + getState().cancelCaption = cancelCaption; + + return this; + } + + @Override + public String getSaveCaption() { + return getState(false).saveCaption; + } + + @Override + public String getCancelCaption() { + return getState(false).cancelCaption; + } + + @Override + protected EditorState getState() { + return getState(true); + } + + @Override + protected EditorState getState(boolean markAsDirty) { + return (EditorState) super.getState(markAsDirty); + } + + @Override + public Editor setErrorGenerator(EditorErrorGenerator errorGenerator) { + Objects.requireNonNull(errorGenerator, "Error generator can't be null"); + this.errorGenerator = errorGenerator; + return this; + } + + @Override + public EditorErrorGenerator getErrorGenerator() { + return errorGenerator; + } +} \ No newline at end of file diff --git a/server/src/test/java/com/vaadin/data/BinderBookOfVaadinTest.java b/server/src/test/java/com/vaadin/data/BinderBookOfVaadinTest.java index a621549b83..14a6be0307 100644 --- a/server/src/test/java/com/vaadin/data/BinderBookOfVaadinTest.java +++ b/server/src/test/java/com/vaadin/data/BinderBookOfVaadinTest.java @@ -637,7 +637,7 @@ public class BinderBookOfVaadinTest { public void withBinderStatusHandlerExample() { Label formStatusLabel = new Label(); - BinderValidationStatusHandler defaultHandler = binder + BinderValidationStatusHandler defaultHandler = binder .getValidationStatusHandler(); binder.setValidationStatusHandler(status -> { -- cgit v1.2.3