diff options
18 files changed, 1874 insertions, 43 deletions
diff --git a/client/src/main/java/com/vaadin/client/connectors/grid/ColumnConnector.java b/client/src/main/java/com/vaadin/client/connectors/grid/ColumnConnector.java index c3aa4fe5d2..700a3de575 100644 --- a/client/src/main/java/com/vaadin/client/connectors/grid/ColumnConnector.java +++ b/client/src/main/java/com/vaadin/client/connectors/grid/ColumnConnector.java @@ -132,6 +132,11 @@ public class ColumnConnector extends AbstractExtensionConnector { column.setExpandRatio(getState().expandRatio); } + @OnStateChange("editable") + void updateEditable() { + column.setEditable(getState().editable); + } + @Override public void onUnregister() { super.onUnregister(); diff --git a/client/src/main/java/com/vaadin/client/connectors/grid/EditorConnector.java b/client/src/main/java/com/vaadin/client/connectors/grid/EditorConnector.java new file mode 100644 index 0000000000..c4bd801022 --- /dev/null +++ b/client/src/main/java/com/vaadin/client/connectors/grid/EditorConnector.java @@ -0,0 +1,222 @@ +/* + * 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.client.connectors.grid; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.client.ComponentConnector; +import com.vaadin.client.ConnectorMap; +import com.vaadin.client.ServerConnector; +import com.vaadin.client.annotations.OnStateChange; +import com.vaadin.client.extensions.AbstractExtensionConnector; +import com.vaadin.client.widget.grid.EditorHandler; +import com.vaadin.client.widgets.Grid; +import com.vaadin.client.widgets.Grid.Column; +import com.vaadin.shared.data.DataCommunicatorConstants; +import com.vaadin.shared.ui.Connect; +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.components.grid.EditorImpl; + +import elemental.json.JsonObject; + +/** + * Connector for Grid Editor. + * + * @author Vaadin Ltd + * @since + */ +@Connect(EditorImpl.class) +public class EditorConnector extends AbstractExtensionConnector { + + /** + * EditorHandler for communicating with the server-side implementation. + */ + private class CustomEditorHandler implements EditorHandler<JsonObject> { + private EditorServerRpc rpc = getRpcProxy(EditorServerRpc.class); + private EditorRequest<JsonObject> currentRequest = null; + private boolean serverInitiated = false; + + public CustomEditorHandler() { + registerRpc(EditorClientRpc.class, new EditorClientRpc() { + @Override + public void cancel() { + serverInitiated = true; + getParent().getWidget().cancelEditor(); + } + + @Override + public void confirmBind(final boolean bindSucceeded) { + endRequest(bindSucceeded); + } + + @Override + public void confirmSave(boolean saveSucceeded) { + endRequest(saveSucceeded); + } + + @Override + public void setErrorMessage(String errorMessage, + List<String> errorColumnsIds) { + Collection<Column<?, JsonObject>> errorColumns; + if (errorColumnsIds != null) { + errorColumns = new ArrayList<Grid.Column<?, JsonObject>>(); + for (String colId : errorColumnsIds) { + errorColumns.add(getParent().getColumn(colId)); + } + } else { + errorColumns = null; + } + getParent().getWidget().getEditor() + .setEditorError(errorMessage, errorColumns); + } + }); + } + + @Override + public void bind(EditorRequest<JsonObject> request) { + startRequest(request); + rpc.bind(getRowKey(request.getRow())); + } + + @Override + public void save(EditorRequest<JsonObject> request) { + startRequest(request); + rpc.save(); + } + + @Override + public void cancel(EditorRequest<JsonObject> request) { + if (!handleServerInitiated(request)) { + // No startRequest as we don't get (or need) + // a confirmation from the server + rpc.cancel(); + } + } + + @Override + public Widget getWidget(Column<?, JsonObject> column) { + String connId = getState().columnFields + .get(getParent().getColumnId(column)); + if (connId == null) { + return null; + } + return getConnector(connId).getWidget(); + } + + private ComponentConnector getConnector(String id) { + return (ComponentConnector) ConnectorMap.get(getConnection()) + .getConnector(id); + } + + /** + * Used to handle the case where the editor calls us because it was + * invoked by the server via RPC and not by the client. In that case, + * the request can be simply synchronously completed. + * + * @param request + * the request object + * @return true if the request was originally triggered by the server, + * false otherwise + */ + private boolean handleServerInitiated(EditorRequest<?> request) { + assert request != null : "Cannot handle null request"; + assert currentRequest == null : "Earlier request not yet finished"; + + if (serverInitiated) { + serverInitiated = false; + request.success(); + return true; + } else { + return false; + } + } + + private void startRequest(EditorRequest<JsonObject> request) { + assert currentRequest == null : "Earlier request not yet finished"; + + currentRequest = request; + } + + private void endRequest(boolean succeeded) { + assert currentRequest != null : "Current request was null"; + /* + * Clear current request first to ensure the state is valid if + * another request is made in the callback. + */ + EditorRequest<JsonObject> request = currentRequest; + currentRequest = null; + if (succeeded) { + request.success(); + } else { + request.failure(); + } + } + } + + @OnStateChange("buffered") + void updateBuffered() { + getParent().getWidget().getEditor().setBuffered(getState().buffered); + } + + @OnStateChange("enabled") + void updateEnabled() { + getParent().getWidget().getEditor().setEnabled(getState().enabled); + } + + @OnStateChange("saveCaption") + void updateSaveCaption() { + getParent().getWidget().getEditor() + .setSaveCaption(getState().saveCaption); + } + + @OnStateChange("cancelCaption") + void updateCancelCaption() { + getParent().getWidget().getEditor() + .setCancelCaption(getState().cancelCaption); + } + + @Override + protected void extend(ServerConnector target) { + Grid<JsonObject> grid = getParent().getWidget(); + grid.getEditor().setHandler(new CustomEditorHandler()); + } + + @Override + public GridConnector getParent() { + return (GridConnector) super.getParent(); + } + + @Override + public EditorState getState() { + return (EditorState) super.getState(); + } + + /** + * Returns the key of the given data row. + * + * @param row + * the row + * @return the row key + */ + protected static String getRowKey(JsonObject row) { + return row.getString(DataCommunicatorConstants.KEY); + } +} diff --git a/client/src/main/java/com/vaadin/client/widget/grid/EditorHandler.java b/client/src/main/java/com/vaadin/client/widget/grid/EditorHandler.java index 764a6e5086..97e8cbf297 100644 --- a/client/src/main/java/com/vaadin/client/widget/grid/EditorHandler.java +++ b/client/src/main/java/com/vaadin/client/widget/grid/EditorHandler.java @@ -19,6 +19,7 @@ import java.util.Collection; import com.google.gwt.user.client.ui.Widget; import com.vaadin.client.widgets.Grid; +import com.vaadin.client.widgets.Grid.Editor; /** * An interface for binding widgets and data to the grid row editor. Used by the @@ -93,6 +94,15 @@ public interface EditorHandler<T> { * Informs Grid that an error occurred while trying to process the * request. * + * @see Editor#setEditorError(String, Collection) + */ + public void failure(); + + /** + * Informs Grid that an error occurred while trying to process the + * request. This method is a short-hand for calling {@link #failure()} + * and {@link Editor#setEditorError(String, Collection)} + * * @param errorMessage * and error message to show to the user, or * <code>null</code> to not show any message. @@ -100,9 +110,14 @@ public interface EditorHandler<T> { * a collection of columns for which an error indicator * should be shown, or <code>null</code> if no columns should * be marked as erroneous. + * + * @see Editor#setEditorError(String, Collection) */ - public void failure(String errorMessage, - Collection<Grid.Column<?, T>> errorColumns); + public default void failure(String errorMessage, + Collection<Grid.Column<?, T>> errorColumns) { + failure(); + getGrid().getEditor().setEditorError(errorMessage, errorColumns); + } /** * Checks whether the request is completed or not. diff --git a/client/src/main/java/com/vaadin/client/widgets/Grid.java b/client/src/main/java/com/vaadin/client/widgets/Grid.java index e1633fdbd3..0cad901ec2 100644 --- a/client/src/main/java/com/vaadin/client/widgets/Grid.java +++ b/client/src/main/java/com/vaadin/client/widgets/Grid.java @@ -1152,9 +1152,8 @@ public class Grid<T> extends ResizeComposite implements HasSelectionHandlers<T>, } @Override - public void failure(String errorMessage, - Collection<Grid.Column<?, T>> errorColumns) { - complete(errorMessage, errorColumns); + public void failure() { + complete("", null); if (callback != null) { callback.onError(this); } 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<BEAN> implements Serializable { private Label statusLabel; - private BinderValidationStatusHandler statusHandler; + private BinderValidationStatusHandler<BEAN> statusHandler; private boolean hasChanges = false; @@ -1356,7 +1356,7 @@ public class Binder<BEAN> implements Serializable { * @see Binding#withValidationStatusHandler(ValidationStatusHandler) */ public void setValidationStatusHandler( - BinderValidationStatusHandler statusHandler) { + BinderValidationStatusHandler<BEAN> statusHandler) { Objects.requireNonNull(statusHandler, "Cannot set a null " + BinderValidationStatusHandler.class.getSimpleName()); if (statusLabel != null) { @@ -1377,7 +1377,7 @@ public class Binder<BEAN> implements Serializable { * @return the status handler used, never <code>null</code> * @see #setValidationStatusHandler(BinderStatusHandler) */ - public BinderValidationStatusHandler getValidationStatusHandler() { + public BinderValidationStatusHandler<BEAN> getValidationStatusHandler() { return Optional.ofNullable(statusHandler) .orElse(this::handleBinderValidationStatus); } @@ -1509,7 +1509,7 @@ public class Binder<BEAN> implements Serializable { * validators */ protected void handleBinderValidationStatus( - BinderValidationStatus<?> binderStatus) { + BinderValidationStatus<BEAN> 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. * <p> - * {{@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. * <p> * 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 <BEAN> + * the bean type of binder + * * @since 8.0 */ -public interface BinderValidationStatusHandler - extends Consumer<BinderValidationStatus<?>>, Serializable { +public interface BinderValidationStatusHandler<BEAN> + extends Consumer<BinderValidationStatus<BEAN>>, 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<T> extends AbstractListing<T> 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<T> extends AbstractListing<T> implements HasComponents { private StyleGenerator<T> styleGenerator = item -> null; private DescriptionGenerator<T> descriptionGenerator; + private SerializableFunction<T, Component> componentGenerator; + /** * Constructs a new Column configuration with given header caption, * renderer and value provider. @@ -1498,8 +1509,85 @@ public class Grid<T> extends AbstractListing<T> implements HasComponents { } /** + * 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<T, V> 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. + * <p> + * <strong>Note:</strong> 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<T, V> 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. + * <p> + * <strong>Note:</strong> The same component cannot be used for multiple + * columns. + * + * @param componentGenerator + * the editor component generator + * @return this column + * + * @see #setEditorComponent(Component) + */ + public Column<T, V> setEditorComponentGenerator( + SerializableFunction<T, Component> componentGenerator) { + Objects.requireNonNull(componentGenerator); + this.componentGenerator = componentGenerator; + return setEditable(true); + } + + /** + * Gets the editor component generator for this Column. + * + * @return editor component generator + */ + public SerializableFunction<T, Component> 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<T> extends AbstractListing<T> 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<T> extends AbstractListing<T> implements HasComponents { public GridStaticCellType getCellType(); } + /** + * Generator for creating editor validation and conversion error messages. + * + * @param <T> + * the bean type + */ + public interface EditorErrorGenerator<T> extends Serializable, + BiFunction<Map<Component, Column<T, ?>>, BinderValidationStatus<T>, 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<Component, Column<T, ?>> fieldToColumn, + BinderValidationStatus<T> status); + } + + /** + * An editor in a Grid. + * + * @param <T> + */ + public interface Editor<T> 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<T> setBinder(Binder<T> binder); + + /** + * Returns the underlying Binder from Editor. + * + * @return the binder; not {@code null} + */ + public Binder<T> 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<T> 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<T> 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<T> setSaveCaption(String saveCaption); + + /** + * Sets the caption of the cancel button in buffered mode. + * + * @param cancelCaption + * the cancel button caption + * @return this editor + */ + public Editor<T> 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. + * <p> + * 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<T> setErrorGenerator( + EditorErrorGenerator<T> errorGenerator); + + /** + * Gets the error message generator of this editor. + * + * @return the function that generates error messages; not {@code null} + * + * @see EditorErrorGenerator + */ + public EditorErrorGenerator<T> getErrorGenerator(); + } + private class HeaderImpl extends Header { @Override @@ -1768,6 +2017,8 @@ public class Grid<T> extends AbstractListing<T> implements HasComponents { private GridSelectionModel<T> selectionModel; + private Editor<T> editor; + /** * Constructor for the {@link Grid} component. */ @@ -1782,6 +2033,11 @@ public class Grid<T> extends AbstractListing<T> 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<T> extends AbstractListing<T> implements HasComponents { return ((SingleSelectionModel<T>) model).asSingleSelect(); } + public Editor<T> getEditor() { + return editor; + } + /** * Sets the selection model for this listing. * <p> @@ -2675,6 +2935,17 @@ public class Grid<T> extends AbstractListing<T> 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<T> 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 <T> + * the grid bean type + */ +public class EditorImpl<T> extends AbstractGridExtension<T> + implements Editor<T> { + + private class EditorStatusHandler + implements BinderValidationStatusHandler<T> { + + @Override + public void accept(BinderValidationStatus<T> 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<Component> fields = status.getFieldValidationErrors() + .stream().map(error -> error.getField()) + .filter(columnFields.values()::contains) + .map(field -> (Component) field) + .collect(Collectors.toList()); + + Map<Component, Column<T, ?>> 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<String> columnIds = fieldToColumn.values().stream() + .map(Column::getId).collect(Collectors.toList()); + + rpc.setErrorMessage(message, columnIds); + } + } + + } + + private Binder<T> binder; + private Map<Column<T, ?>, Component> columnFields = new HashMap<>(); + private T edited; + private boolean saving = false; + private EditorClientRpc rpc; + private EditorErrorGenerator<T> 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<T> setBinder(Binder<T> binder) { + this.binder = binder; + + binder.setValidationStatusHandler(new EditorStatusHandler()); + return this; + } + + @Override + public Binder<T> getBinder() { + return binder; + } + + @Override + public Editor<T> setBuffered(boolean buffered) { + if (isOpen()) { + throw new IllegalStateException( + "Cannot modify Editor when it is open."); + } + getState().buffered = buffered; + + return this; + } + + @Override + public Editor<T> 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<T> setSaveCaption(String saveCaption) { + Objects.requireNonNull(saveCaption); + getState().saveCaption = saveCaption; + + return this; + } + + @Override + public Editor<T> 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<T> setErrorGenerator(EditorErrorGenerator<T> errorGenerator) { + Objects.requireNonNull(errorGenerator, "Error generator can't be null"); + this.errorGenerator = errorGenerator; + return this; + } + + @Override + public EditorErrorGenerator<T> 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<BookPerson> defaultHandler = binder .getValidationStatusHandler(); binder.setValidationStatusHandler(status -> { diff --git a/shared/src/main/java/com/vaadin/shared/ui/grid/ColumnState.java b/shared/src/main/java/com/vaadin/shared/ui/grid/ColumnState.java index 8296dd8660..c2f4f6a07e 100644 --- a/shared/src/main/java/com/vaadin/shared/ui/grid/ColumnState.java +++ b/shared/src/main/java/com/vaadin/shared/ui/grid/ColumnState.java @@ -23,6 +23,7 @@ public class ColumnState extends SharedState { public String caption; public String id; public boolean sortable; + public boolean editable = false; /** The caption for the column hiding toggle. */ public String hidingToggleCaption; @@ -58,4 +59,5 @@ public class ColumnState extends SharedState { public boolean resizable = true; public Connector renderer; + } diff --git a/shared/src/main/java/com/vaadin/shared/ui/grid/editor/EditorClientRpc.java b/shared/src/main/java/com/vaadin/shared/ui/grid/editor/EditorClientRpc.java new file mode 100644 index 0000000000..04cdac431b --- /dev/null +++ b/shared/src/main/java/com/vaadin/shared/ui/grid/editor/EditorClientRpc.java @@ -0,0 +1,64 @@ +/* + * 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.editor; + +import java.util.List; + +import com.vaadin.shared.communication.ClientRpc; + +/** + * An RPC interface for the grid editor server-to-client communications. + * + * @since + * @author Vaadin Ltd + */ +public interface EditorClientRpc extends ClientRpc { + + /** + * Tells the client to cancel editing and hide the editor. + */ + void cancel(); + + /** + * Confirms a pending {@link EditorServerRpc#bind(String) bind request} sent + * by the client. + * + * @param bindSucceeded + * {@code true} if and only if the bind action was successful + */ + void confirmBind(boolean bindSucceeded); + + /** + * Confirms a pending {@link EditorServerRpc#save() save request} sent by + * the client. + * + * @param saveSucceeded + * {@code true} if and only if the save action was successful + */ + void confirmSave(boolean saveSucceeded); + + /** + * Sets the displayed error messages for editor. + * + * @param errorMessage + * the error message to show the user; {@code} null to clear + * @param errorColumnsIds + * a list of column ids that should get error markers; empty list + * to clear + * + */ + void setErrorMessage(String errorMessage, List<String> errorColumnsIds); +} diff --git a/shared/src/main/java/com/vaadin/shared/ui/grid/editor/EditorServerRpc.java b/shared/src/main/java/com/vaadin/shared/ui/grid/editor/EditorServerRpc.java new file mode 100644 index 0000000000..47f6d365cc --- /dev/null +++ b/shared/src/main/java/com/vaadin/shared/ui/grid/editor/EditorServerRpc.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.shared.ui.grid.editor; + +import com.vaadin.shared.communication.ServerRpc; + +/** + * An RPC interface for the grid editor client-to-server communications. + * + * @since + * @author Vaadin Ltd + */ +public interface EditorServerRpc extends ServerRpc { + + /** + * Asks the server to open the editor and bind data to it. When a bind + * request is sent, it must be acknowledged with a + * {@link EditorClientRpc#confirmBind() confirm call} before the client can + * open the editor. + * + * @param key + * the identifier key for edited item + */ + void bind(String key); + + /** + * Asks the server to save unsaved changes in the editor to the bean. When a + * save request is sent, it must be acknowledged with a + * {@link EditorClientRpc#confirmSave() confirm call}. + */ + void save(); + + /** + * Tells the server to cancel editing. When sending a cancel request, the + * client does not need to wait for confirmation by the server before hiding + * the editor. + */ + void cancel(); +} diff --git a/shared/src/main/java/com/vaadin/shared/ui/grid/editor/EditorState.java b/shared/src/main/java/com/vaadin/shared/ui/grid/editor/EditorState.java new file mode 100644 index 0000000000..1cae17e135 --- /dev/null +++ b/shared/src/main/java/com/vaadin/shared/ui/grid/editor/EditorState.java @@ -0,0 +1,49 @@ +/* + * 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.editor; + +import java.util.HashMap; +import java.util.Map; + +import com.vaadin.shared.communication.SharedState; +import com.vaadin.shared.ui.grid.GridConstants; + +/** + * State object for Editor in Grid. + * + * @author Vaadin Ltd + * @since + */ +public class EditorState extends SharedState { + + { + // Disable editor by default. + enabled = false; + } + + /** Map from Column id to Component connector id. */ + public Map<String, String> columnFields = new HashMap<>(); + + /** Buffer mode state. */ + public boolean buffered = true; + + /** The caption for the save button in the editor. */ + public String saveCaption = GridConstants.DEFAULT_SAVE_CAPTION; + + /** The caption for the cancel button in the editor. */ + public String cancelCaption = GridConstants.DEFAULT_CANCEL_CAPTION; + +} 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 f5f02bbf60..8917ce0241 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 @@ -1,6 +1,21 @@ package com.vaadin.tests.components.grid.basics; +import java.text.DecimalFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Stream; + +import com.vaadin.annotations.Theme; import com.vaadin.annotations.Widgetset; +import com.vaadin.data.Binder; +import com.vaadin.data.util.converter.StringToIntegerConverter; import com.vaadin.server.VaadinRequest; import com.vaadin.shared.Registration; import com.vaadin.shared.ui.grid.HeightMode; @@ -19,6 +34,7 @@ import com.vaadin.ui.MenuBar.MenuItem; import com.vaadin.ui.Notification; import com.vaadin.ui.Panel; import com.vaadin.ui.StyleGenerator; +import com.vaadin.ui.TextField; import com.vaadin.ui.VerticalLayout; import com.vaadin.ui.components.grid.SingleSelectionModel; import com.vaadin.ui.renderers.DateRenderer; @@ -26,19 +42,8 @@ import com.vaadin.ui.renderers.HtmlRenderer; import com.vaadin.ui.renderers.NumberRenderer; import com.vaadin.ui.renderers.ProgressBarRenderer; -import java.text.DecimalFormat; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.function.Consumer; -import java.util.stream.Stream; - @Widgetset("com.vaadin.DefaultWidgetSet") +@Theme("tests-valo-disabled-animations") public class GridBasics extends AbstractTestUIWithLog { public static final String ROW_STYLE_GENERATOR_ROW_NUMBERS_FOR_3_OF_4 = "Row numbers for 3/4"; @@ -52,9 +57,9 @@ public class GridBasics extends AbstractTestUIWithLog { public static final String CELL_STYLE_GENERATOR_EMPTY = "Empty string"; public static final String CELL_STYLE_GENERATOR_NULL = "Null"; - public static final String[] COLUMN_CAPTIONS = {"Column 0", "Column 1", + public static final String[] COLUMN_CAPTIONS = { "Column 0", "Column 1", "Column 2", "Row Number", "Date", "HTML String", "Big Random", - "Small Random"}; + "Small Random" }; private final Command toggleReorderListenerCommand = new Command() { private Registration registration = null; @@ -144,7 +149,6 @@ public class GridBasics extends AbstractTestUIWithLog { private List<DataObject> data; private int watchingCount = 0; private PersistingDetailsGenerator persistingDetails; - private List<Column<DataObject, ?>> initialColumnOrder; public GridBasics() { generators.put("NULL", null); @@ -169,24 +173,47 @@ public class GridBasics extends AbstractTestUIWithLog { // Create grid grid = new Grid<>(); grid.setItems(data); - - grid.addColumn(dataObj -> "(" + dataObj.getRowNumber() + ", 0)") - .setCaption(COLUMN_CAPTIONS[0]); + grid.setSizeFull(); + + Binder<DataObject> binder = grid.getEditor().getBinder(); + + TextField html = new TextField(); + TextField smallRandom = new TextField(); + TextField coordinates = new TextField(); + TextField rowNumber = new TextField(); + + binder.bind(html, DataObject::getHtmlString, DataObject::setHtmlString); + binder.forField(smallRandom) + .withConverter(new StringToIntegerConverter( + "Could not convert value to Integer")) + .withValidator(i -> i >= 0 && i < 5, + "Small random needs to be in range [0..5)") + .bind(DataObject::getSmallRandom, DataObject::setSmallRandom); + binder.bind(coordinates, DataObject::getCoordinates, + DataObject::setCoordinates); + binder.forField(rowNumber) + .withConverter(new StringToIntegerConverter( + "Could not convert value to Integer")) + .bind(DataObject::getRowNumber, DataObject::setRowNumber); + + grid.addColumn(DataObject::getCoordinates) + .setCaption(COLUMN_CAPTIONS[0]).setEditorComponent(coordinates); grid.addColumn(dataObj -> "(" + dataObj.getRowNumber() + ", 1)") .setCaption(COLUMN_CAPTIONS[1]); grid.addColumn(dataObj -> "(" + dataObj.getRowNumber() + ", 2)") .setCaption(COLUMN_CAPTIONS[2]); grid.addColumn(DataObject::getRowNumber, new NumberRenderer()) - .setCaption(COLUMN_CAPTIONS[3]); + .setCaption(COLUMN_CAPTIONS[3]).setEditorComponent(rowNumber); grid.addColumn(DataObject::getDate, new DateRenderer()) .setCaption(COLUMN_CAPTIONS[4]); grid.addColumn(DataObject::getHtmlString, new HtmlRenderer()) - .setCaption(COLUMN_CAPTIONS[5]); + .setCaption(COLUMN_CAPTIONS[5]).setEditorComponent(html); grid.addColumn(DataObject::getBigRandom, new NumberRenderer()) .setCaption(COLUMN_CAPTIONS[6]); grid.addColumn(data -> data.getSmallRandom() / 5d, - new ProgressBarRenderer()).setCaption(COLUMN_CAPTIONS[7]); + new ProgressBarRenderer()).setCaption(COLUMN_CAPTIONS[7]) + .setEditorComponent(smallRandom); ((SingleSelectionModel<DataObject>) grid.getSelectionModel()) .addSelectionChangeListener( @@ -199,6 +226,9 @@ public class GridBasics extends AbstractTestUIWithLog { private Component createMenu() { MenuBar menu = new MenuBar(); + menu.setErrorHandler(error -> log("Exception occured, " + + error.getThrowable().getClass().getName() + ": " + + error.getThrowable().getMessage())); MenuItem componentMenu = menu.addItem("Component", null); createStateMenu(componentMenu.addItem("State", null)); createSizeMenu(componentMenu.addItem("Size", null)); @@ -207,6 +237,7 @@ public class GridBasics extends AbstractTestUIWithLog { createHeaderMenu(componentMenu.addItem("Header", null)); createFooterMenu(componentMenu.addItem("Footer", null)); createColumnsMenu(componentMenu.addItem("Columns", null)); + createEditorMenu(componentMenu.addItem("Editor", null)); return menu; } @@ -274,9 +305,8 @@ public class GridBasics extends AbstractTestUIWithLog { selectedItem -> col .setHidden(selectedItem.isChecked())) .setCheckable(true); - columnMenu - .addItem("Remove", - selectedItem -> grid.removeColumn(col)); + columnMenu.addItem("Remove", + selectedItem -> grid.removeColumn(col)); } } @@ -353,6 +383,11 @@ public class GridBasics extends AbstractTestUIWithLog { .addItem("Column Reordering", selectedItem -> grid .setColumnReorderingAllowed(selectedItem.isChecked())) .setCheckable(true); + + MenuItem enableItem = stateMenu.addItem("Enabled", + e -> grid.setEnabled(e.isChecked())); + enableItem.setCheckable(true); + enableItem.setChecked(true); } private void createRowStyleMenu(MenuItem rowStyleMenu) { @@ -401,7 +436,7 @@ public class GridBasics extends AbstractTestUIWithLog { } private <T> void addGridMethodMenu(MenuItem parent, String name, T value, - Consumer<T> method) { + Consumer<T> method) { parent.addItem(name, menuItem -> method.accept(value)); } @@ -453,7 +488,8 @@ public class GridBasics extends AbstractTestUIWithLog { }); } - private void mergeHeaderСells(int rowIndex, String jointCellText, int... columnIndexes) { + private void mergeHeaderСells(int rowIndex, String jointCellText, + int... columnIndexes) { HeaderRow headerRow = grid.getHeaderRow(rowIndex); List<Column<DataObject, ?>> columns = grid.getColumns(); Set<Grid.HeaderCell> toMerge = new HashSet<>(); @@ -522,8 +558,27 @@ public class GridBasics extends AbstractTestUIWithLog { }); } + private void createEditorMenu(MenuItem editorMenu) { + editorMenu + .addItem("Enabled", + i -> grid.getEditor().setEnabled(i.isChecked())) + .setCheckable(true); + MenuItem bufferedMode = editorMenu.addItem("Buffered mode", + i -> grid.getEditor().setBuffered(i.isChecked())); + bufferedMode.setCheckable(true); + bufferedMode.setChecked(true); + + editorMenu.addItem("Save", i -> grid.getEditor().save()); + editorMenu.addItem("Cancel edit", i -> grid.getEditor().cancel()); + + editorMenu.addItem("Change save caption", + e -> grid.getEditor().setSaveCaption("ǝʌɐS")); + editorMenu.addItem("Change cancel caption", + e -> grid.getEditor().setCancelCaption("ʃǝɔuɐↃ")); + + } + private void openOrCloseDetails(DataObject dataObj) { grid.setDetailsVisible(dataObj, !grid.isDetailsVisible(dataObj)); } - } diff --git a/uitest/src/test/java/com/vaadin/tests/components/grid/basics/GridBasicsTest.java b/uitest/src/test/java/com/vaadin/tests/components/grid/basics/GridBasicsTest.java index c128ba2634..55098eb768 100644 --- a/uitest/src/test/java/com/vaadin/tests/components/grid/basics/GridBasicsTest.java +++ b/uitest/src/test/java/com/vaadin/tests/components/grid/basics/GridBasicsTest.java @@ -16,6 +16,7 @@ import org.openqa.selenium.remote.DesiredCapabilities; import com.vaadin.testbench.TestBenchElement; import com.vaadin.testbench.customelements.GridElement; import com.vaadin.testbench.elements.GridElement.GridCellElement; +import com.vaadin.testbench.elements.GridElement.GridEditorElement; import com.vaadin.testbench.parallel.Browser; import com.vaadin.tests.tb3.MultiBrowserTest; import com.vaadin.v7.tests.components.grid.basicfeatures.GridBasicFeaturesTest.CellSide; @@ -204,4 +205,13 @@ public abstract class GridBasicsTest extends MultiBrowserTest { return null; } + protected GridEditorElement getEditor() { + return getGridElement().getEditor(); + } + + protected int getGridVerticalScrollPos() { + return ((Number) executeScript("return arguments[0].scrollTop", + getGridVerticalScrollbar())).intValue(); + } + } diff --git a/uitest/src/test/java/com/vaadin/tests/components/grid/basics/GridEditorBufferedTest.java b/uitest/src/test/java/com/vaadin/tests/components/grid/basics/GridEditorBufferedTest.java new file mode 100644 index 0000000000..17411c5a77 --- /dev/null +++ b/uitest/src/test/java/com/vaadin/tests/components/grid/basics/GridEditorBufferedTest.java @@ -0,0 +1,290 @@ +/* + * 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 static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; + +import org.junit.Before; +import org.junit.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.Keys; +import org.openqa.selenium.NoSuchElementException; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.interactions.Actions; + +import com.vaadin.shared.ui.grid.GridConstants; +import com.vaadin.testbench.TestBenchElement; +import com.vaadin.testbench.elements.GridElement.GridCellElement; +import com.vaadin.testbench.elements.GridElement.GridEditorElement; +import com.vaadin.testbench.elements.NotificationElement; + +public class GridEditorBufferedTest extends GridEditorTest { + + @Override + @Before + public void setUp() { + super.setUp(); + } + + @Test + public void testKeyboardSave() { + editRow(100); + + WebElement textField = getEditor().getField(0); + + textField.click(); + // without this, the click in the middle of the field might not be after + // the old text on some browsers + new Actions(getDriver()).sendKeys(Keys.END).perform(); + + textField.sendKeys(" changed"); + + // Save from keyboard + new Actions(getDriver()).sendKeys(Keys.ENTER).perform(); + + assertEditorClosed(); + assertEquals("(100, 0) changed", + getGridElement().getCell(100, 0).getText()); + } + + @Test + public void testKeyboardSaveWithInvalidEdition() { + makeInvalidEdition(); + + GridEditorElement editor = getGridElement().getEditor(); + TestBenchElement field = editor.getField(7); + + field.click(); + new Actions(getDriver()).sendKeys(Keys.ENTER).perform(); + + assertEditorOpen(); + assertEquals( + GridBasics.COLUMN_CAPTIONS[7] + + ": Could not convert value to Integer", + editor.getErrorMessage()); + assertTrue("Field 7 should have been marked with an error after error", + isEditorCellErrorMarked(7)); + + editor.cancel(); + + editRow(100); + assertFalse("Exception should not exist", + isElementPresent(NotificationElement.class)); + assertEquals("There should be no editor error message", null, + getGridElement().getEditor().getErrorMessage()); + } + + @Test + public void testSave() { + editRow(100); + + WebElement textField = getEditor().getField(0); + + textField.click(); + // without this, the click in the middle of the field might not be after + // the old text on some browsers + new Actions(getDriver()).sendKeys(Keys.END).perform(); + + textField.sendKeys(" changed"); + + WebElement saveButton = getEditor() + .findElement(By.className("v-grid-editor-save")); + + saveButton.click(); + + assertEquals("(100, 0) changed", + getGridElement().getCell(100, 0).getText()); + } + + @Test + public void testProgrammaticSave() { + editRow(100); + + WebElement textField = getEditor().getField(0); + + textField.click(); + // without this, the click in the middle of the field might not be after + // the old text on some browsers + new Actions(getDriver()).sendKeys(Keys.END).perform(); + + textField.sendKeys(" changed"); + + selectMenuPath("Component", "Editor", "Save"); + + assertEquals("(100, 0) changed", + getGridElement().getCell(100, 0).getText()); + } + + @Test + public void testInvalidEdition() { + makeInvalidEdition(); + + GridEditorElement editor = getGridElement().getEditor(); + editor.save(); + + assertEquals( + GridBasics.COLUMN_CAPTIONS[7] + + ": Could not convert value to Integer", + editor.getErrorMessage()); + assertTrue("Field 7 should have been marked with an error after error", + isEditorCellErrorMarked(7)); + editor.cancel(); + + editRow(100); + assertFalse("Exception should not exist", + isElementPresent(NotificationElement.class)); + assertEquals("There should be no editor error message", null, + getGridElement().getEditor().getErrorMessage()); + } + + private void makeInvalidEdition() { + editRow(5); + assertFalse(logContainsText( + "Exception occured, java.lang.IllegalStateException")); + + GridEditorElement editor = getGridElement().getEditor(); + + assertFalse( + "Field 7 should not have been marked with an error before error", + editor.isFieldErrorMarked(7)); + + WebElement intField = editor.getField(7); + intField.clear(); + intField.sendKeys("banana phone"); + editor.getField(5).click(); + } + + @Test + public void testEditorInDisabledGrid() { + int originalScrollPos = getGridVerticalScrollPos(); + + editRow(5); + assertEditorOpen(); + + selectMenuPath("Component", "State", "Enabled"); + assertEditorOpen(); + + GridEditorElement editor = getGridElement().getEditor(); + editor.save(); + assertEditorOpen(); + + editor.cancel(); + assertEditorOpen(); + + selectMenuPath("Component", "State", "Enabled"); + + scrollGridVerticallyTo(100); + assertEquals( + "Grid shouldn't scroll vertically while editing in buffered mode", + originalScrollPos, getGridVerticalScrollPos()); + } + + @Test + public void testCaptionChange() { + editRow(5); + assertEquals("Save button caption should've been \"" + + GridConstants.DEFAULT_SAVE_CAPTION + "\" to begin with", + GridConstants.DEFAULT_SAVE_CAPTION, getSaveButton().getText()); + assertEquals("Cancel button caption should've been \"" + + GridConstants.DEFAULT_CANCEL_CAPTION + "\" to begin with", + GridConstants.DEFAULT_CANCEL_CAPTION, + getCancelButton().getText()); + + selectMenuPath("Component", "Editor", "Change save caption"); + assertNotEquals( + "Save button caption should've changed while editor is open", + GridConstants.DEFAULT_SAVE_CAPTION, getSaveButton().getText()); + + getCancelButton().click(); + + selectMenuPath("Component", "Editor", "Change cancel caption"); + editRow(5); + assertNotEquals( + "Cancel button caption should've changed while editor is closed", + GridConstants.DEFAULT_CANCEL_CAPTION, + getCancelButton().getText()); + } + + @Test(expected = NoSuchElementException.class) + public void testVerticalScrollLocking() { + editRow(5); + getGridElement().getCell(200, 0); + } + + @Test + public void testScrollDisabledOnMouseOpen() { + int originalScrollPos = getGridVerticalScrollPos(); + + GridCellElement cell_5_0 = getGridElement().getCell(5, 0); + new Actions(getDriver()).doubleClick(cell_5_0).perform(); + + scrollGridVerticallyTo(100); + assertEquals( + "Grid shouldn't scroll vertically while editing in buffered mode", + originalScrollPos, getGridVerticalScrollPos()); + } + + @Test + public void testScrollDisabledOnKeyboardOpen() { + int originalScrollPos = getGridVerticalScrollPos(); + + GridCellElement cell_5_0 = getGridElement().getCell(5, 0); + cell_5_0.click(); + new Actions(getDriver()).sendKeys(Keys.ENTER).perform(); + + scrollGridVerticallyTo(100); + assertEquals( + "Grid shouldn't scroll vertically while editing in buffered mode", + originalScrollPos, getGridVerticalScrollPos()); + } + + @Test + public void testMouseOpeningClosing() { + + getGridElement().getCell(4, 0).doubleClick(); + assertEditorOpen(); + + getCancelButton().click(); + assertEditorClosed(); + + selectMenuPath(TOGGLE_EDIT_ENABLED); + getGridElement().getCell(4, 0).doubleClick(); + assertEditorClosed(); + } + + @Test + public void testMouseOpeningDisabledWhenOpen() { + editRow(5); + + getGridElement().getCell(2, 0).doubleClick(); + + assertEquals("Editor should still edit row 5", "(5, 0)", + getEditor().getField(0).getAttribute("value")); + } + + @Test + public void testUserSortDisabledWhenOpen() { + editRow(5); + + getGridElement().getHeaderCell(0, 0).click(); + + assertEditorOpen(); + assertEquals("(2, 0)", getGridElement().getCell(2, 0).getText()); + } +} diff --git a/uitest/src/test/java/com/vaadin/tests/components/grid/basics/GridEditorTest.java b/uitest/src/test/java/com/vaadin/tests/components/grid/basics/GridEditorTest.java new file mode 100644 index 0000000000..fdf1b9bba9 --- /dev/null +++ b/uitest/src/test/java/com/vaadin/tests/components/grid/basics/GridEditorTest.java @@ -0,0 +1,238 @@ +/* + * 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 static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.openqa.selenium.Keys; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.interactions.Actions; + +import com.vaadin.testbench.By; +import com.vaadin.testbench.TestBenchElement; +import com.vaadin.testbench.elements.GridElement.GridCellElement; +import com.vaadin.testbench.elements.GridElement.GridEditorElement; + +public abstract class GridEditorTest extends GridBasicsTest { + + protected static final org.openqa.selenium.By BY_EDITOR_CANCEL = By + .className("v-grid-editor-cancel"); + protected static final org.openqa.selenium.By BY_EDITOR_SAVE = By + .className("v-grid-editor-save"); + protected static final String[] TOGGLE_EDIT_ENABLED = new String[] { + "Component", "Editor", "Enabled" }; + + @Override + @Before + public void setUp() { + setDebug(true); + openTestURL(); + selectMenuPath(TOGGLE_EDIT_ENABLED); + } + + @Test + public void testProgrammaticClosing() { + editRow(5); + assertEditorOpen(); + + selectMenuPath("Component", "Editor", "Cancel edit"); + assertEditorClosed(); + } + + @Test + public void testKeyboardOpeningClosing() { + + getGridElement().getCell(4, 0).click(); + assertEditorClosed(); + + new Actions(getDriver()).sendKeys(Keys.ENTER).perform(); + assertEditorOpen(); + + new Actions(getDriver()).sendKeys(Keys.ESCAPE).perform(); + assertEditorClosed(); + + // Disable Editor + selectMenuPath(TOGGLE_EDIT_ENABLED); + getGridElement().getCell(5, 0).click(); + new Actions(getDriver()).sendKeys(Keys.ENTER).perform(); + assertEditorClosed(); + } + + protected void assertEditorOpen() { + assertTrue("Editor is supposed to be open", + getGridElement().isElementPresent(By.vaadin("#editor"))); + } + + protected void assertEditorClosed() { + assertFalse("Editor is supposed to be closed", + getGridElement().isElementPresent(By.vaadin("#editor"))); + } + + @Test + public void testFocusOnMouseOpen() { + + GridCellElement cell = getGridElement().getCell(4, 0); + + cell.doubleClick(); + + WebElement focused = getFocusedElement(); + + assertEquals("", "input", focused.getTagName()); + assertEquals("", cell.getText(), focused.getAttribute("value")); + } + + @Test + public void testFocusOnKeyboardOpen() { + + GridCellElement cell = getGridElement().getCell(4, 0); + + cell.click(); + new Actions(getDriver()).sendKeys(Keys.ENTER).perform(); + + WebElement focused = getFocusedElement(); + + assertEquals("", "input", focused.getTagName()); + assertEquals("", cell.getText(), focused.getAttribute("value")); + } + + @Test + public void testUneditableColumn() { + editRow(5); + assertEditorOpen(); + + GridEditorElement editor = getGridElement().getEditor(); + assertFalse("Uneditable column should not have an editor widget", + editor.isEditable(2)); + + String classNames = editor + .findElements(By.className("v-grid-editor-cells")).get(1) + .findElements(By.xpath("./div")).get(2).getAttribute("class"); + + assertTrue("Noneditable cell should contain not-editable classname", + classNames.contains("not-editable")); + + assertTrue("Noneditable cell should contain v-grid-cell classname", + classNames.contains("v-grid-cell")); + + assertNoErrorNotifications(); + } + + @Test + public void testNoOpenFromHeaderOrFooter() { + selectMenuPath("Component", "Footer", "Append footer row"); + + getGridElement().getHeaderCell(0, 0).doubleClick(); + assertEditorClosed(); + + new Actions(getDriver()).sendKeys(Keys.ENTER).perform(); + assertEditorClosed(); + + getGridElement().getFooterCell(0, 0).doubleClick(); + assertEditorClosed(); + + new Actions(getDriver()).sendKeys(Keys.ENTER).perform(); + assertEditorClosed(); + } + + public void testEditorMoveOnResize() { + selectMenuPath("Component", "Size", "Height", "500px"); + getGridElement().getCell(22, 0).doubleClick(); + assertEditorOpen(); + + GridEditorElement editor = getGridElement().getEditor(); + TestBenchElement tableWrapper = getGridElement().getTableWrapper(); + + int tableWrapperBottom = tableWrapper.getLocation().getY() + + tableWrapper.getSize().getHeight(); + int editorBottom = editor.getLocation().getY() + + editor.getSize().getHeight(); + + assertTrue("Editor should not be initially outside grid", + tableWrapperBottom - editorBottom <= 2); + + selectMenuPath("Component", "Size", "Height", "300px"); + assertEditorOpen(); + + tableWrapperBottom = tableWrapper.getLocation().getY() + + tableWrapper.getSize().getHeight(); + editorBottom = editor.getLocation().getY() + + editor.getSize().getHeight(); + + assertTrue("Editor should not be outside grid after resize", + tableWrapperBottom - editorBottom <= 2); + } + + public void testEditorDoesNotMoveOnResizeIfNotNeeded() { + selectMenuPath("Component", "Size", "Height", "500px"); + + editRow(5); + assertEditorOpen(); + + GridEditorElement editor = getGridElement().getEditor(); + + int editorPos = editor.getLocation().getY(); + + selectMenuPath("Component", "Size", "Height", "300px"); + assertEditorOpen(); + + assertTrue("Editor should not have moved due to resize", + editorPos == editor.getLocation().getY()); + } + + @Ignore("Needs programmatic sorting") + @Test + public void testEditorClosedOnSort() { + editRow(5); + + selectMenuPath("Component", "State", "Sort by column", "Column 0, ASC"); + + assertEditorClosed(); + } + + @Ignore("Needs programmatic filtering") + @Test + public void testEditorClosedOnFilter() { + editRow(5); + + selectMenuPath("Component", "Filter", "Column 1 starts with \"(23\""); + + assertEditorClosed(); + } + + protected WebElement getSaveButton() { + return getDriver().findElement(BY_EDITOR_SAVE); + } + + protected WebElement getCancelButton() { + return getDriver().findElement(BY_EDITOR_CANCEL); + } + + protected void editRow(int rowIndex) { + getGridElement().getCell(rowIndex, 0).doubleClick(); + assertEditorOpen(); + } + + protected boolean isEditorCellErrorMarked(int colIndex) { + WebElement editorCell = getGridElement().getEditor() + .findElement(By.xpath("./div/div[" + (colIndex + 1) + "]")); + return editorCell.getAttribute("class").contains("error"); + } +} diff --git a/uitest/src/test/java/com/vaadin/tests/components/grid/basics/GridEditorUnbufferedTest.java b/uitest/src/test/java/com/vaadin/tests/components/grid/basics/GridEditorUnbufferedTest.java new file mode 100644 index 0000000000..aac69d90f5 --- /dev/null +++ b/uitest/src/test/java/com/vaadin/tests/components/grid/basics/GridEditorUnbufferedTest.java @@ -0,0 +1,246 @@ +/* + * 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 static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.Keys; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.interactions.Actions; + +import com.vaadin.testbench.TestBenchElement; +import com.vaadin.testbench.elements.GridElement.GridCellElement; + +public class GridEditorUnbufferedTest extends GridEditorTest { + + private static final String[] TOGGLE_EDITOR_BUFFERED = new String[] { + "Component", "Editor", "Buffered mode" }; + private static final String[] CANCEL_EDIT = new String[] { "Component", + "Editor", "Cancel edit" }; + + @Override + @Before + public void setUp() { + super.setUp(); + selectMenuPath(TOGGLE_EDITOR_BUFFERED); + } + + @Test + public void testEditorShowsNoButtons() { + editRow(5); + + assertEditorOpen(); + + assertFalse("Save button should not be visible in unbuffered mode.", + isElementPresent(BY_EDITOR_SAVE)); + + assertFalse("Cancel button should not be visible in unbuffered mode.", + isElementPresent(BY_EDITOR_CANCEL)); + } + + @Test + public void testToggleEditorUnbufferedWhileOpen() { + editRow(5); + assertEditorOpen(); + selectMenuPath(TOGGLE_EDITOR_BUFFERED); + boolean thrown = logContainsText( + "Exception occured, java.lang.IllegalStateException"); + assertTrue("IllegalStateException was not thrown", thrown); + } + + @Test + public void testEditorMoveWithMouse() { + editRow(5); + + assertEditorOpen(); + + String firstFieldValue = getEditor().getField(0).getAttribute("value"); + assertEquals("Editor should be at row 5", "(5, 0)", firstFieldValue); + + getGridElement().getCell(6, 0).click(); + firstFieldValue = getEditor().getField(0).getAttribute("value"); + + assertEquals("Editor should be at row 6", "(6, 0)", firstFieldValue); + } + + @Test + public void testEditorMoveWithKeyboard() throws InterruptedException { + editRow(100); + + getEditor().getField(0).click(); + new Actions(getDriver()).sendKeys(Keys.ENTER).perform(); + + String firstFieldValue = getEditor().getField(0).getAttribute("value"); + assertEquals("Editor should move to row 101", "(101, 0)", + firstFieldValue); + + for (int i = 0; i < 10; i++) { + new Actions(getDriver()).keyDown(Keys.SHIFT).sendKeys(Keys.ENTER) + .keyUp(Keys.SHIFT).perform(); + + firstFieldValue = getEditor().getField(0).getAttribute("value"); + int row = 100 - i; + assertEquals("Editor should move to row " + row, "(" + row + ", 0)", + firstFieldValue); + } + } + + @Test + public void testValidationErrorPreventsMove() throws InterruptedException { + editRow(5); + + getEditor().getField(7).click(); + String faultyInt = "not a number"; + getEditor().getField(7).sendKeys(faultyInt); + + getGridElement().getCell(6, 7).click(); + + assertEquals("Editor should not move from row 5", "(5, 0)", + getEditor().getField(0).getAttribute("value")); + + getEditor().getField(7).sendKeys(Keys.chord(Keys.CONTROL, "a")); + getEditor().getField(7).sendKeys("4"); + + getGridElement().getCell(7, 0).click(); + + assertEquals("Editor should move to row 7", "(7, 0)", + getEditor().getField(0).getAttribute("value")); + + } + + @Test + public void testErrorMessageWrapperHidden() { + editRow(5); + + assertEditorOpen(); + + WebElement editorFooter = getEditor() + .findElement(By.className("v-grid-editor-footer")); + + assertTrue("Editor footer should not be visible when there's no error", + editorFooter.getCssValue("display").equalsIgnoreCase("none")); + } + + @Test + public void testScrollEnabledOnMouseOpen() { + int originalScrollPos = getGridVerticalScrollPos(); + + GridCellElement cell_5_0 = getGridElement().getCell(5, 0); + new Actions(getDriver()).doubleClick(cell_5_0).perform(); + + scrollGridVerticallyTo(100); + assertGreater( + "Grid should scroll vertically while editing in unbuffered mode", + getGridVerticalScrollPos(), originalScrollPos); + } + + @Test + public void testScrollEnabledOnKeyboardOpen() { + int originalScrollPos = getGridVerticalScrollPos(); + + GridCellElement cell_5_0 = getGridElement().getCell(5, 0); + cell_5_0.click(); + new Actions(getDriver()).sendKeys(Keys.ENTER).perform(); + + scrollGridVerticallyTo(100); + assertGreater( + "Grid should scroll vertically while editing in unbuffered mode", + getGridVerticalScrollPos(), originalScrollPos); + } + + @Test + public void testEditorInDisabledGrid() { + editRow(5); + + selectMenuPath("Component", "State", "Enabled"); + assertEditorOpen(); + + assertTrue("Editor text field should be disabled", + null != getEditor().getField(0).getAttribute("disabled")); + + selectMenuPath("Component", "State", "Enabled"); + assertEditorOpen(); + + assertFalse("Editor text field should not be disabled", + null != getEditor().getField(0).getAttribute("disabled")); + } + + @Test + public void testMouseOpeningClosing() { + + getGridElement().getCell(4, 0).doubleClick(); + assertEditorOpen(); + + selectMenuPath(CANCEL_EDIT); + selectMenuPath(TOGGLE_EDIT_ENABLED); + + getGridElement().getCell(4, 0).doubleClick(); + assertEditorClosed(); + } + + @Ignore("Needs refresh item functionality") + @Test + public void testExternalValueChangePassesToEditor() { + editRow(5); + assertEditorOpen(); + + selectMenuPath("Component", "State", "ReactiveValueChanger"); + + getEditor().getField(0).click(); + getEditor().getField(0).sendKeys("changing value"); + + // Focus another field to cause the value to be sent to the server + getEditor().getField(3).click(); + + assertEquals("Value of Column 2 in the editor was not changed", + "Modified", getEditor().getField(5).getAttribute("value")); + } + + @Test + public void testEditorClosedOnUserSort() { + editRow(5); + + getGridElement().getHeaderCell(0, 0).click(); + + assertEditorClosed(); + } + + @Test + public void testEditorSaveOnRowChange() { + // Double click sets the focus programmatically + getGridElement().getCell(5, 0).doubleClick(); + + TestBenchElement editor = getGridElement().getEditor().getField(0); + editor.clear(); + // Click to ensure IE focus... + editor.click(5, 5); + editor.sendKeys("Foo", Keys.ENTER); + + assertEquals("Editor did not move.", "(6, 0)", + getGridElement().getEditor().getField(0).getAttribute("value")); + assertEquals("Editor field value did not update from server.", "6", + getGridElement().getEditor().getField(3).getAttribute("value")); + + assertEquals("Edited value was not saved.", "Foo", + getGridElement().getCell(5, 0).getText()); + } +}
\ No newline at end of file |